From 52aaa9f15d21f7ec185e5b6ba776d461ebaedcd1 Mon Sep 17 00:00:00 2001 From: zhongjin Date: Sat, 13 Jun 2020 18:34:34 +0800 Subject: [PATCH] Initial commit --- .editorconfig | 15 + .eslintignore | 7 + .eslintrc.json | 14 + .github/ISSUE_TEMPLATE.md | 29 + .github/stale.yml | 53 + .gitignore | 22 + .hound.yml | 5 + .npmrc | 1 + .scss-lint.yml | 184 + .travis.yml | 21 + CHANGELOG.md | 199 + CHANGELOG_INTERNAL.md | 160 + Gruntfile.js | 155 + LICENSE | 29 + README.md | 105 + SECURITY-POLICY.md | 3 + config/jsdoc/index.html | 18 + config/jsdoc/internal-conf.json | 20 + config/jsdoc/plugins/api.js | 26 + config/jsdoc/public-conf.json | 21 + docs/guides/01-overview.md | 172 + docs/guides/02-get-api-key.md | 60 + docs/guides/03-quickstart.md | 408 + docs/guides/04-performance-tips.md | 101 + docs/guides/05-upgrade-considerations.md | 181 + docs/guides/06-glossary.md | 35 + docs/guides/quickstart-example.html | 203 + docs/img/avatar.gif | Bin 0 -> 191484 bytes docs/img/set_content_diagram.svg | 19 + docs/img/set_query_diagram.svg | 21 + docs/img/set_table_name_diagram.svg | 21 + docs/reference/01-introduction.md | 9 + docs/reference/02-authentication.md | 16 + docs/reference/03-versioning.md | 10 + docs/reference/04-loading-the-library.md | 20 + docs/reference/05-error-handling.md | 20 + docs/support/01-support-options.md | 38 + docs/support/02-faq.md | 51 + docs/support/03-error-messages.md | 372 + docs/support/04-browser-support.md | 15 + docs/support/05-release-announcement.md | 17 + docs/support/06-contribute.md | 36 + examples/acceptance/displaced-popups.html | 109 + examples/acceptance/multiclient-gmaps.html | 88 + examples/examples.json | 268 + examples/index.html | 44 + examples/internal/basemap-selector.html | 351 + examples/internal/gmaps-geometry-editor.html | 596 + .../internal/leaflet-geometry-editor.html | 607 + examples/internal/leaflet-vector.html | 269 + examples/internal/search-geocode.html | 77 + examples/internal/viz/000.json | 203 + examples/internal/viz/001.json | 170 + examples/internal/viz/002.json | 262 + examples/internal/viz/003.json | 53 + examples/internal/vizjson.html | 89 + examples/public/dataviews/category.html | 99 + examples/public/dataviews/error-handling.html | 107 + examples/public/dataviews/formula.html | 86 + examples/public/dataviews/histogram.html | 99 + examples/public/dataviews/time-series.html | 98 + examples/public/filters/and-filter.html | 153 + .../public/filters/bounding-box-gmaps.html | 115 + .../public/filters/bounding-box-leaflet.html | 104 + examples/public/filters/category-filter.html | 113 + examples/public/filters/circle-filter.html | 334 + examples/public/filters/complex-filter.html | 90 + .../filters/custom-bounding-box-leaflet.html | 139 + examples/public/filters/polygon-filter.html | 332 + examples/public/filters/range-filter.html | 105 + .../layers/change-feature-columns-gmaps.html | 117 + .../change-feature-columns-leaflet.html | 107 + .../public/layers/change-order-gmaps.html | 113 + .../public/layers/change-order-leaflet.html | 103 + .../public/layers/change-source-gmaps.html | 114 + .../public/layers/change-source-leaflet.html | 100 + .../public/layers/change-style-gmaps.html | 117 + .../public/layers/change-style-leaflet.html | 107 + .../public/layers/feature-click-gmaps.html | 89 + .../public/layers/feature-click-leaflet.html | 77 + .../public/layers/feature-over-out-gmaps.html | 111 + .../layers/feature-over-out-leaflet.html | 102 + .../layer-with-aggregation-cluster-gmaps.html | 98 + ...ayer-with-aggregation-cluster-leaflet.html | 90 + .../layers/layer-with-aggregation-gmaps.html | 82 + .../layer-with-aggregation-leaflet.html | 73 + .../layers/layer-with-zoom-options-gmaps.html | 51 + .../layer-with-zoom-options-leaflet.html | 43 + examples/public/layers/multilayer-gmaps.html | 85 + .../public/layers/multilayer-leaflet.html | 76 + .../public/layers/single-layer-gmaps.html | 67 + .../public/layers/single-layer-leaflet.html | 61 + examples/public/misc/boilerplate-gmaps.html | 15 + examples/public/misc/boilerplate-leaflet.html | 16 + .../misc/custom-search-dataset-gmaps.html | 178 + .../misc/custom-search-dataset-leaflet.html | 147 + .../public/misc/edit-sql-cartocss-gmaps.html | 119 + .../misc/edit-sql-cartocss-leaflet.html | 111 + .../public/misc/error-handling-gmaps.html | 201 + .../public/misc/error-handling-leaflet.html | 190 + .../public/misc/filter-data-map-gmaps.html | 118 + .../public/misc/filter-data-map-leaflet.html | 125 + examples/public/misc/guide-gmaps.html | 218 + examples/public/misc/guide-leaflet.html | 217 + .../misc/hexagon-aggregation-gmaps.html | 133 + .../misc/hexagon-aggregation-leaflet.html | 127 + examples/public/misc/legends-gmaps.html | 193 + examples/public/misc/legends-leaflet.html | 185 + examples/public/misc/piechart-vega.html | 219 + .../public/misc/populated-places-gmaps.html | 140 + .../public/misc/populated-places-leaflet.html | 132 + examples/public/misc/popups-gmaps.html | 134 + examples/public/misc/popups-leaflet.html | 129 + examples/public/misc/popups-video-gmaps.html | 97 + .../public/misc/popups-video-leaflet.html | 92 + examples/public/misc/storymap-leaflet.html | 194 + examples/public/style.css | 487 + grunt/README.md | 20 + grunt/tasks/_browserify-bundles.js | 45 + grunt/tasks/browserify.js | 34 + grunt/tasks/clean.js | 21 + grunt/tasks/concat.js | 15 + grunt/tasks/connect.js | 21 + grunt/tasks/copy.js | 12 + grunt/tasks/cssmin.js | 28 + grunt/tasks/exorcise.js | 24 + grunt/tasks/fastly.js | 21 + grunt/tasks/imagemin.js | 21 + grunt/tasks/jasmine.js | 32 + grunt/tasks/s3.js | 68 + grunt/tasks/scss.js | 29 + grunt/tasks/terser.js | 29 + grunt/tasks/usebanner.js | 24 + grunt/tasks/watch.js | 27 + gulpfile.js | 21 + index.html | 50 + package-lock.json | 15982 ++++++++++++++++ package.json | 173 + scripts/deploy-docs-examples.sh | 82 + scripts/generate-package-json.js | 14 + scripts/package.public.json | 16 + scripts/release.sh | 32 + secrets.example.json | 7 + security.txt | 4 + src/analysis/analysis-model.js | 229 + src/analysis/analysis-poller.js | 60 + src/analysis/analysis-service.js | 155 + src/analysis/camshaft-reference.js | 51 + src/api/create-vis.js | 171 + src/api/promise.js | 17 + src/api/sql.js | 704 + src/api/v4/README.md | 47 + src/api/v4/client.js | 521 + src/api/v4/constants.js | 106 + src/api/v4/dataview/base.js | 490 + src/api/v4/dataview/category/index.js | 268 + src/api/v4/dataview/category/parse-data.js | 60 + src/api/v4/dataview/formula/index.js | 134 + src/api/v4/dataview/formula/parse-data.js | 29 + src/api/v4/dataview/histogram/index.js | 228 + src/api/v4/dataview/histogram/parse-data.js | 91 + src/api/v4/dataview/index.js | 19 + src/api/v4/dataview/time-series/index.js | 230 + src/api/v4/dataview/time-series/parse-data.js | 73 + .../v4/error-handling/carto-error-extender.js | 92 + src/api/v4/error-handling/carto-error.js | 137 + .../error-handling/carto-validation-error.js | 17 + .../error-handling/error-list/ajax-errors.js | 3 + src/api/v4/error-handling/error-list/index.js | 9 + .../error-list/validation-errors.js | 272 + .../error-list/windshaft-errors.js | 72 + src/api/v4/error-handling/error-tracker.js | 15 + src/api/v4/events.js | 19 + src/api/v4/filter/and.js | 34 + src/api/v4/filter/base-sql.js | 195 + src/api/v4/filter/base.js | 45 + src/api/v4/filter/bounding-box-gmaps.js | 72 + src/api/v4/filter/bounding-box-leaflet.js | 71 + src/api/v4/filter/bounding-box.js | 93 + src/api/v4/filter/category.js | 107 + src/api/v4/filter/circle.js | 90 + src/api/v4/filter/filters-collection.js | 112 + src/api/v4/filter/index.js | 25 + src/api/v4/filter/or.js | 38 + src/api/v4/filter/polygon.js | 91 + src/api/v4/filter/range.js | 140 + src/api/v4/filter/spatial-filter-types.js | 10 + src/api/v4/index.js | 50 + src/api/v4/layer/aggregation.js | 178 + src/api/v4/layer/base.js | 51 + src/api/v4/layer/event-types.js | 27 + src/api/v4/layer/index.js | 18 + src/api/v4/layer/layer.js | 576 + src/api/v4/layer/metadata/base.js | 56 + src/api/v4/layer/metadata/buckets.js | 95 + src/api/v4/layer/metadata/categories.js | 68 + src/api/v4/layer/metadata/parser.js | 37 + src/api/v4/layers.js | 45 + src/api/v4/native/google-maps-map-type.js | 70 + src/api/v4/native/leaflet-layer.js | 107 + .../v4/native/trigger-layer-feature-event.js | 57 + src/api/v4/source/base.js | 137 + src/api/v4/source/dataset.js | 128 + src/api/v4/source/index.js | 11 + src/api/v4/source/sql.js | 137 + src/api/v4/style/base.js | 33 + src/api/v4/style/cartocss.js | 96 + src/api/v4/style/index.js | 9 + src/api/vizjson.js | 133 + src/cartodb.js | 27 + src/cdb.config.js | 9 + src/cdb.js | 10 + src/cdb.log.js | 19 + src/cdb.templates.js | 3 + src/constants.js | 28 + src/core/config.js | 16 + src/core/loader.js | 62 + src/core/model.js | 81 + src/core/profiler.js | 160 + src/core/sanitize.js | 24 + src/core/template-list.js | 28 + src/core/template.js | 92 + src/core/util.js | 195 + src/core/view.js | 173 + src/dataviews/category-dataview-model.js | 320 + .../categories-collection.js | 50 + .../category-dataview/category-item-model.js | 14 + .../category-dataview/category-model-range.js | 60 + .../category-dataview/search-model.js | 127 + src/dataviews/dataview-model-base.js | 572 + src/dataviews/dataviews-collection.js | 24 + src/dataviews/dataviews-factory.js | 96 + src/dataviews/formula-dataview-model.js | 44 + src/dataviews/helpers/histogram-helper.js | 130 + src/dataviews/histogram-dataview-model.js | 444 + .../histogram-data-model.js | 155 + src/engine.js | 463 + .../adapters/gmaps-bounding-box-adapter.js | 52 + .../adapters/leaflet-bounding-box-adapter.js | 40 + .../map-model-bounding-box-adapter.js | 39 + src/geo/cartodb-layer-group-view-base.js | 96 + src/geo/cartodb-layer-group.js | 250 + src/geo/geocoder/mapbox-geocoder.js | 81 + src/geo/geocoder/tomtom-geocoder.js | 95 + src/geo/geometry-models/geojson-helper.js | 85 + src/geo/geometry-models/geometry-base.js | 45 + src/geo/geometry-models/geometry-factory.js | 91 + .../geometry-models/multi-geometry-base.js | 52 + src/geo/geometry-models/multi-point.js | 37 + src/geo/geometry-models/multi-polygon.js | 40 + src/geo/geometry-models/multi-polyline.js | 40 + src/geo/geometry-models/path-base.js | 75 + src/geo/geometry-models/point.js | 49 + src/geo/geometry-models/polygon.js | 38 + src/geo/geometry-models/polyline.js | 32 + .../geometry-views/base/geometry-view-base.js | 18 + .../base/marker-adapter-base.js | 47 + .../base/multi-geometry-view-base.js | 27 + .../geometry-views/base/path-adapter-base.js | 23 + src/geo/geometry-views/base/path-view-base.js | 207 + .../geometry-views/base/point-view-base.js | 151 + .../geometry-views/geometry-view-factory.js | 90 + .../geometry-views/gmaps/gmaps-coordinates.js | 12 + .../gmaps/gmaps-marker-adapter.js | 70 + .../gmaps/gmaps-path-adapter.js | 39 + .../geometry-views/gmaps/multi-point-view.js | 8 + .../gmaps/multi-polygon-view.js | 8 + .../gmaps/multi-polyline-view.js | 8 + src/geo/geometry-views/gmaps/point-view.js | 27 + src/geo/geometry-views/gmaps/polygon-view.js | 23 + src/geo/geometry-views/gmaps/polyline-view.js | 23 + .../leaflet/leaflet-marker-adapter.js | 65 + .../leaflet/leaflet-path-adapter.js | 41 + .../leaflet/multi-point-view.js | 8 + .../leaflet/multi-polygon-view.js | 8 + .../leaflet/multi-polyline-view.js | 8 + src/geo/geometry-views/leaflet/point-view.js | 21 + .../geometry-views/leaflet/polygon-view.js | 19 + .../geometry-views/leaflet/polyline-view.js | 19 + src/geo/gmaps/gmaps-base-layer-view.js | 44 + .../gmaps/gmaps-cartodb-layer-group-view.js | 360 + src/geo/gmaps/gmaps-layer-view-factory.js | 46 + src/geo/gmaps/gmaps-layer-view.js | 29 + src/geo/gmaps/gmaps-map-view.js | 218 + src/geo/gmaps/gmaps-plain-layer-view.js | 34 + src/geo/gmaps/gmaps-tiled-layer-view.js | 42 + src/geo/gmaps/gmaps-torque-layer-view.js | 45 + src/geo/gmaps/projector.js | 27 + .../leaflet-cartodb-layer-group-view.js | 126 + src/geo/leaflet/leaflet-layer-view-factory.js | 43 + src/geo/leaflet/leaflet-layer-view.js | 53 + src/geo/leaflet/leaflet-map-view.js | 301 + src/geo/leaflet/leaflet-plain-layer-view.js | 46 + src/geo/leaflet/leaflet-tiled-layer-view.js | 76 + src/geo/leaflet/leaflet-torque-layer-view.js | 36 + src/geo/leaflet/leaflet-wms-layer-view.js | 46 + src/geo/map-view-factory.js | 48 + src/geo/map-view.js | 362 + src/geo/map.js | 515 + src/geo/map/cartodb-layer.js | 170 + src/geo/map/gmaps-base-layer.js | 13 + src/geo/map/infowindow-template.js | 47 + src/geo/map/layer-model-base.js | 77 + src/geo/map/layer-types.js | 36 + src/geo/map/layers.js | 100 + src/geo/map/legends/bubble-legend-model.js | 27 + src/geo/map/legends/category-legend-model.js | 24 + .../map/legends/choropleth-legend-model.js | 28 + .../legends/custom-choropleth-legend-model.js | 17 + src/geo/map/legends/custom-legend-model.js | 14 + src/geo/map/legends/legend-model-base.js | 76 + src/geo/map/legends/legends.js | 112 + .../map/legends/static-legend-model-base.js | 16 + src/geo/map/legends/torque-legend-model.js | 14 + src/geo/map/plain-layer.js | 42 + src/geo/map/popup-fields.js | 19 + src/geo/map/tile-layer.js | 35 + src/geo/map/tooltip-template.js | 48 + src/geo/map/torque-layer.js | 224 + src/geo/map/wms-layer.js | 20 + src/geo/render-modes.js | 5 + src/geo/torque-layer-view-base.js | 211 + .../ui/attribution/attribution-template.tpl | 4 + src/geo/ui/attribution/attribution-view.js | 98 + src/geo/ui/default-infowindow-template.tpl | 25 + src/geo/ui/infowindow-model.js | 154 + src/geo/ui/infowindow-view.js | 676 + src/geo/ui/legends/base/img-loader-view.js | 85 + src/geo/ui/legends/base/legend-title.tpl | 3 + src/geo/ui/legends/base/legend-view-base.js | 107 + src/geo/ui/legends/bubble/legend-template.tpl | 37 + src/geo/ui/legends/bubble/legend-view.js | 112 + .../legends/bubble/placeholder-template.tpl | 25 + .../ui/legends/categories/legend-template.tpl | 14 + src/geo/ui/legends/categories/legend-view.js | 25 + .../categories/placeholder-template.tpl | 14 + .../ui/legends/choropleth/legend-template.tpl | 16 + src/geo/ui/legends/choropleth/legend-view.js | 50 + .../choropleth/placeholder-template.tpl | 13 + .../custom-choropleth/legend-template.tpl | 10 + .../legends/custom-choropleth/legend-view.js | 23 + src/geo/ui/legends/custom/legend-template.tpl | 14 + src/geo/ui/legends/custom/legend-view.js | 24 + src/geo/ui/legends/layer-legends-template.tpl | 17 + src/geo/ui/legends/layer-legends-view.js | 123 + src/geo/ui/legends/legend-view-factory.js | 36 + src/geo/ui/legends/legends-view.js | 156 + src/geo/ui/legends/legends-view.tpl | 1 + src/geo/ui/limits/limits-view.js | 22 + src/geo/ui/logo-view.js | 15 + src/geo/ui/logo.tpl | 8 + src/geo/ui/overlays-container.tpl | 1 + src/geo/ui/overlays-view.js | 171 + src/geo/ui/search/search.js | 237 + .../ui/search/search_infowindow_template.tpl | 15 + src/geo/ui/search/search_template.tpl | 6 + src/geo/ui/tiles-loader.js | 49 + src/geo/ui/tiles/tiles-template.tpl | 18 + src/geo/ui/tiles/tiles-view.js | 92 + src/geo/ui/tooltip-model.js | 81 + src/geo/ui/tooltip-view.js | 149 + src/geo/ui/zoom/zoom-template.tpl | 5 + src/geo/ui/zoom/zoom-view.js | 63 + src/index.js | 11 + .../common/fullscreen/fullscreen-template.tpl | 6 + src/ui/common/fullscreen/fullscreen-view.js | 118 + src/util/backbone-abort-sync.js | 17 + src/util/date-utils.js | 9 + src/util/formatter.js | 72 + src/util/get-object-value.js | 17 + src/util/tile-error.tpl | 27 + src/vis/dataviews-tracker.js | 17 + src/vis/drawing-controller.js | 35 + src/vis/edition-controller.js | 37 + src/vis/geometry-management-controller.js | 48 + src/vis/image/queue.js | 45 + src/vis/infowindow-manager.js | 144 + src/vis/layers-factory.js | 153 + src/vis/map-cursor-manager.js | 85 + src/vis/map-events-manager.js | 54 + src/vis/overlays-factory.js | 50 + src/vis/overlays/attribution.js | 13 + src/vis/overlays/custom.js | 7 + src/vis/overlays/fullscreen.js | 27 + src/vis/overlays/index.js | 12 + src/vis/overlays/layer_selector.js | 3 + src/vis/overlays/limits.js | 13 + src/vis/overlays/loader.js | 9 + src/vis/overlays/logo.js | 9 + src/vis/overlays/search.js | 22 + src/vis/overlays/tiles.js | 13 + src/vis/overlays/zoom.js | 13 + src/vis/settings.js | 10 + src/vis/tooltip-manager.js | 97 + src/vis/vis-view.js | 132 + src/vis/vis.js | 416 + src/vis/vis/infowindow-template.js | 23 + .../legends/rule-to-bubble-legend-adapter.js | 38 + .../rule-to-category-legend-adapter.js | 62 + .../rule-to-choropleth-legend-adapter.js | 54 + .../legends/rule-to-legend-model-adapters.js | 11 + src/windshaft-integration/legends/rule.js | 66 + src/windshaft-integration/model-updater.js | 266 + src/windshaft/client.js | 184 + src/windshaft/config.js | 3 + src/windshaft/error-parser.js | 31 + src/windshaft/error.js | 61 + src/windshaft/filters/base.js | 17 + src/windshaft/filters/bounding-box.js | 55 + src/windshaft/filters/category.js | 144 + src/windshaft/filters/circle.js | 20 + src/windshaft/filters/polygon.js | 20 + src/windshaft/filters/range.js | 31 + .../analysis-serializer.js | 42 + .../anonymous-map-serializer.js | 19 + .../dataviews-serializer.js | 10 + .../layers-serializer.js | 125 + .../named-map-serializer.js | 35 + src/windshaft/request-tracker.js | 47 + src/windshaft/request.js | 18 + src/windshaft/response.js | 147 + test/fail-tests-if-have-errors-in-src.js | 21 + test/helpers/SpecHelper.js | 1 + test/helpers/mockFactory.js | 59 + test/install-source-map-support.js | 6 + test/spec/analysis/analysis-model.spec.js | 516 + test/spec/analysis/analysis-poller.spec.js | 105 + test/spec/analysis/analysis-service.spec.js | 357 + test/spec/analysis/camshaft-reference.spec.js | 19 + test/spec/api/createVis/create-vis.spec.js | 247 + .../api/createVis/scenarios/basic_vis.json.js | 314 + test/spec/api/createVis/scenarios/index.js | 19 + test/spec/api/sql.spec.js | 499 + test/spec/api/v4/client.spec.js | 459 + test/spec/api/v4/dataview/base.spec.js | 243 + test/spec/api/v4/dataview/category.spec.js | 438 + test/spec/api/v4/dataview/formula.spec.js | 265 + test/spec/api/v4/dataview/histogram.spec.js | 452 + test/spec/api/v4/dataview/time-series.spec.js | 427 + .../carto-error-extender.spec.js | 49 + .../api/v4/error-handling/carto-error.spec.js | 107 + .../carto-validation-error.spec.js | 12 + test/spec/api/v4/filter/base-sql.spec.js | 250 + .../api/v4/filter/bounding-box-gmaps.spec.js | 11 + .../v4/filter/bounding-box-leaflet.spec.js | 11 + test/spec/api/v4/filter/bounding-box.spec.js | 50 + test/spec/api/v4/filter/category.spec.js | 63 + test/spec/api/v4/filter/circle.spec.js | 56 + .../api/v4/filter/filters-collection.spec.js | 174 + test/spec/api/v4/filter/polygon.spec.js | 75 + test/spec/api/v4/filter/range.spec.js | 73 + test/spec/api/v4/layer/aggregation.spec.js | 171 + test/spec/api/v4/layer/layer.spec.js | 677 + .../spec/api/v4/layer/metadata/parser.spec.js | 230 + test/spec/api/v4/layers.spec.js | 41 + .../v4/native/google-maps-map-type.spec.js | 141 + test/spec/api/v4/native/leaflet-layer.spec.js | 198 + test/spec/api/v4/source/dataset.spec.js | 268 + test/spec/api/v4/source/sql.spec.js | 257 + test/spec/api/v4/style/cartocss.spec.js | 161 + test/spec/api/vizjson.spec.js | 352 + test/spec/cartodb.mod.torque.spec.js | 20 + test/spec/cartodb.spec.js | 147 + test/spec/core/model.spec.js | 132 + test/spec/core/sanitize.spec.js | 58 + test/spec/core/template-list.spec.js | 21 + test/spec/core/template.spec.js | 31 + test/spec/core/util.spec.js | 107 + test/spec/core/view.spec.js | 165 + .../dataviews/category-dataview-model.spec.js | 452 + .../categories-collection.spec.js | 53 + .../dataviews/dataview-model-base.spec.js | 670 + .../dataviews/dataviews-collection.spec.js | 22 + test/spec/dataviews/dataviews-factory.spec.js | 85 + .../dataviews/formula-dataview-model.spec.js | 140 + .../dataviews/helpers/histogram-helper.js | 110 + .../histogram-dataview-model.spec.js | 986 + .../histogram-data-model.spec.js | 167 + test/spec/engine.spec.js | 568 + test/spec/fixtures/engine.fixture.js | 31 + test/spec/geo/cartodb-layer-group.spec.js | 414 + .../geocoder/mapbox-geocoder-response-0.json | 261 + .../geocoder/mapbox-geocoder-response-1.json | 268 + .../spec/geo/geocoder/mapbox-geocoder.spec.js | 67 + .../geocoder/tomtom-geocoder-response-0.json | 445 + .../spec/geo/geocoder/tomtom-geocoder.spec.js | 51 + .../geometry-models/geometry-factory.spec.js | 148 + .../geo/geometry-models/multi-point.spec.js | 32 + .../geo/geometry-models/multi-polygon.spec.js | 54 + .../geometry-models/multi-polyline.spec.js | 54 + test/spec/geo/geometry-models/point.spec.js | 58 + test/spec/geo/geometry-models/polygon.spec.js | 66 + .../spec/geo/geometry-models/polyline.spec.js | 64 + .../geometry-views/coordinates-comparator.js | 15 + .../geo/geometry-views/create-map-view.js | 69 + .../gmaps/multi-point-view.spec.js | 7 + .../gmaps/multi-polygon-view.spec.js | 7 + .../gmaps/multi-polyline-view.spec.js | 7 + .../geometry-views/gmaps/point-view.spec.js | 7 + .../geometry-views/gmaps/polygon-view.spec.js | 7 + .../gmaps/polyline-view.spec.js | 7 + .../leaflet/multi-point-view.spec.js | 7 + .../leaflet/multi-polygon-view.spec.js | 7 + .../leaflet/multi-polyline-view.spec.js | 7 + .../geometry-views/leaflet/point-view.spec.js | 7 + .../leaflet/polygon-view.spec.js | 7 + .../leaflet/polyline-view.spec.js | 7 + .../shared-tests-for-multi-geometry-views.js | 31 + .../shared-tests-for-multi-point-views.js | 27 + .../shared-tests-for-multi-polygon-views.js | 38 + .../shared-tests-for-multi-polyline-views.js | 38 + .../shared-tests-for-path-views.js | 299 + .../shared-tests-for-point-views.js | 191 + .../shared-tests-for-polygon-views.js | 96 + .../shared-tests-for-polyline-views.js | 90 + .../gmaps/gmaps-cartodb-layer-group.spec.js | 139 + test/spec/geo/gmaps/gmaps-map-view.spec.js | 156 + .../geo/gmaps/gmaps-torque-layer-view.spec.js | 69 + test/spec/geo/gmaps_cartodb_layer/hide.js | 87 + .../geo/gmaps_cartodb_layer/interaction.js | 48 + test/spec/geo/gmaps_cartodb_layer/opacity.js | 73 + test/spec/geo/gmaps_cartodb_layer/show.js | 86 + .../leaflet-cartodb-layer-group-view.spec.js | 50 + .../geo/leaflet/leaflet-layer-view.spec.js | 54 + .../spec/geo/leaflet/leaflet-map-view.spec.js | 253 + .../leaflet/leaflet-tiled-layer-view.spec.js | 132 + .../leaflet/leaflet-torque-layer-view.spec.js | 101 + test/spec/geo/map-view.spec.js | 257 + test/spec/geo/map.spec.js | 510 + test/spec/geo/map/cartodb-layer.spec.js | 240 + test/spec/geo/map/gmaps-base-layer.spec.js | 8 + test/spec/geo/map/infowindow-template.spec.js | 106 + test/spec/geo/map/layer-model-base.spec.js | 85 + test/spec/geo/map/layers.spec.js | 164 + .../geo/map/legends/legend-model-base.spec.js | 24 + test/spec/geo/map/legends/legends.spec.js | 121 + .../map/legends/static-legend-model.spec.js | 20 + test/spec/geo/map/plain-layer.spec.js | 29 + .../geo/map/shared-for-interactive-layers.js | 76 + test/spec/geo/map/tile-layer.spec.js | 23 + test/spec/geo/map/tooltip-template.spec.js | 106 + test/spec/geo/map/torque-layer.spec.js | 100 + .../geo/shared-tests-for-carto-layer-group.js | 254 + .../spec/geo/shared-tests-for-torque-layer.js | 169 + test/spec/geo/torque-layer-view-base.spec.js | 100 + test/spec/geo/ui/attribution-view.spec.js | 121 + test/spec/geo/ui/infowindow-model.spec.js | 173 + test/spec/geo/ui/infowindow-view.spec.js | 603 + .../ui/legends/base/legend-view-base.spec.js | 132 + .../geo/ui/legends/bubble/legend-view.spec.js | 85 + .../choropleth/custom-legend-view.spec.js | 67 + .../ui/legends/choropleth/legend-view.spec.js | 78 + .../ui/legends/custom/img-loader-view.spec.js | 131 + .../geo/ui/legends/custom/legend-view.spec.js | 65 + .../geo/ui/legends/layer-legends-view.spec.js | 109 + test/spec/geo/ui/legends/legends-view.spec.js | 103 + test/spec/geo/ui/limits-view.spec.js | 95 + test/spec/geo/ui/overlays-view.spec.js | 217 + test/spec/geo/ui/search.spec.js | 281 + test/spec/geo/ui/tooltip.spec.js | 173 + test/spec/geo/ui/zoom.spec.js | 84 + test/spec/util/backbone-abort-sync.spec.js | 17 + test/spec/util/formatter.spec.js | 56 + test/spec/vis/dataviews-tracker.spec.js | 59 + test/spec/vis/infowindow-manager.spec.js | 578 + test/spec/vis/layers-factory.spec.js | 199 + test/spec/vis/map-cursor-manager.spec.js | 146 + test/spec/vis/overlays-factory.spec.js | 63 + test/spec/vis/tooltip-manager.spec.js | 343 + test/spec/vis/vis-view.spec.js | 140 + test/spec/vis/vis.spec.js | 1226 ++ .../rule-to-bubble-legend-adapter.spec.js | 95 + .../rule-to-category-legend-adapter.spec.js | 94 + .../rule-to-choropleth-legend-adapter.spec.js | 97 + .../model-updater.spec.js | 733 + test/spec/windshaft/client.spec.js | 292 + test/spec/windshaft/error-parser.spec.js | 82 + test/spec/windshaft/error.mock.js | 3 + test/spec/windshaft/error.spec.js | 55 + test/spec/windshaft/filters/base.spec.js | 26 + test/spec/windshaft/filters/category.spec.js | 226 + test/spec/windshaft/filters/range.spec.js | 65 + .../analysis-serializer.spec.js | 356 + .../anonymous-map-serializer.spec.js | 110 + .../dataviews-serializer.spec.js | 116 + .../layers-serializer.spec.js | 214 + .../named-map-serializer.spec.js | 60 + test/spec/windshaft/request-tracker.spec.js | 42 + test/spec/windshaft/response.mock.js | 60 + test/spec/windshaft/response.spec.js | 371 + themes/img/bg-attribution-button.png | Bin 0 -> 2859 bytes themes/img/bubbles.png | Bin 0 -> 1038 bytes themes/img/burguer.png | Bin 0 -> 2859 bytes themes/img/burguer@2x.png | Bin 0 -> 2844 bytes themes/img/dark.png | Bin 0 -> 1438 bytes themes/img/dark_bubbles.png | Bin 0 -> 3874 bytes themes/img/dark_loader.gif | Bin 0 -> 18715 bytes themes/img/dark_loader@2x.gif | Bin 0 -> 22235 bytes themes/img/default-marker-icon.png | Bin 0 -> 552 bytes themes/img/facebook.png | Bin 0 -> 3017 bytes themes/img/fullscreen.png | Bin 0 -> 2948 bytes themes/img/fullscreen_mobile.png | Bin 0 -> 3274 bytes themes/img/handle.png | Bin 0 -> 2843 bytes themes/img/handle@2x.png | Bin 0 -> 2910 bytes themes/img/headers.png | Bin 0 -> 11915 bytes themes/img/icon-save.svg | 17 + themes/img/icon-share.svg | 19 + themes/img/image_not_found.png | Bin 0 -> 54374 bytes themes/img/layers-2x.png | Bin 0 -> 2898 bytes themes/img/layers.png | Bin 0 -> 1502 bytes themes/img/light.png | Bin 0 -> 1704 bytes themes/img/link.png | Bin 0 -> 3071 bytes themes/img/loader.gif | Bin 0 -> 673 bytes themes/img/loader@2x.gif | Bin 0 -> 2512 bytes themes/img/mobile_zoom.png | Bin 0 -> 3041 bytes themes/img/other.png | Bin 0 -> 1677 bytes themes/img/other@2x.png | Bin 0 -> 6785 bytes themes/img/shadow.png | Bin 0 -> 2944 bytes themes/img/share.png | Bin 0 -> 3114 bytes themes/img/share@2x.png | Bin 0 -> 3148 bytes themes/img/slide_left.png | Bin 0 -> 2969 bytes themes/img/slide_left@2x.png | Bin 0 -> 493 bytes themes/img/slide_right.png | Bin 0 -> 2967 bytes themes/img/slide_right@2x.png | Bin 0 -> 3106 bytes themes/img/slider.png | Bin 0 -> 726 bytes themes/img/toggle_aside.png | Bin 0 -> 3267 bytes themes/img/twitter.png | Bin 0 -> 3012 bytes themes/scss/entry.scss | 29 + .../_cartodb-infowindow-default.scss | 430 + .../_cartodb-infowindow-legacy.scss | 152 + themes/scss/infowindow/themes/_custom.scss | 34 + themes/scss/infowindow/themes/_dark.scss | 75 + themes/scss/infowindow/themes/_light.scss | 55 + themes/scss/map/_attributions.scss | 80 + themes/scss/map/_cartodb-map-light.scss | 1669 ++ themes/scss/map/_limits.scss | 85 + themes/scss/map/_map.scss | 5 + themes/scss/map/_overlays.scss | 540 + .../tooltip/_cartodb-tooltip-default.scss | 82 + themes/scss/tooltip/themes/_dark.scss | 14 + themes/scss/tooltip/themes/_light.scss | 11 + themes/scss/vendor/_leaflet.scss | 480 + vendor/GeoJSON.js | 229 + vendor/backbone.js | 1894 ++ vendor/html-css-sanitizer-bundle.js | 4850 +++++ vendor/jquery.min.js | 4 + vendor/json2.js | 486 + vendor/lzma.js | 3881 ++++ vendor/mod/carto.js | 4 + vendor/mousewheel.js | 84 + vendor/mustache.js | 585 + vendor/mwheelIntent.js | 77 + webpack/banner.js | 6 + webpack/webpack.config.js | 40 + webpack/webpack.min.config.js | 45 + 655 files changed, 96796 insertions(+) create mode 100644 .editorconfig create mode 100644 .eslintignore create mode 100644 .eslintrc.json create mode 100644 .github/ISSUE_TEMPLATE.md create mode 100644 .github/stale.yml create mode 100644 .gitignore create mode 100644 .hound.yml create mode 100644 .npmrc create mode 100644 .scss-lint.yml create mode 100644 .travis.yml create mode 100644 CHANGELOG.md create mode 100644 CHANGELOG_INTERNAL.md create mode 100644 Gruntfile.js create mode 100644 LICENSE create mode 100644 README.md create mode 100644 SECURITY-POLICY.md create mode 100644 config/jsdoc/index.html create mode 100644 config/jsdoc/internal-conf.json create mode 100644 config/jsdoc/plugins/api.js create mode 100644 config/jsdoc/public-conf.json create mode 100644 docs/guides/01-overview.md create mode 100644 docs/guides/02-get-api-key.md create mode 100644 docs/guides/03-quickstart.md create mode 100644 docs/guides/04-performance-tips.md create mode 100644 docs/guides/05-upgrade-considerations.md create mode 100644 docs/guides/06-glossary.md create mode 100644 docs/guides/quickstart-example.html create mode 100644 docs/img/avatar.gif create mode 100644 docs/img/set_content_diagram.svg create mode 100644 docs/img/set_query_diagram.svg create mode 100644 docs/img/set_table_name_diagram.svg create mode 100644 docs/reference/01-introduction.md create mode 100644 docs/reference/02-authentication.md create mode 100644 docs/reference/03-versioning.md create mode 100644 docs/reference/04-loading-the-library.md create mode 100644 docs/reference/05-error-handling.md create mode 100644 docs/support/01-support-options.md create mode 100644 docs/support/02-faq.md create mode 100644 docs/support/03-error-messages.md create mode 100644 docs/support/04-browser-support.md create mode 100644 docs/support/05-release-announcement.md create mode 100644 docs/support/06-contribute.md create mode 100644 examples/acceptance/displaced-popups.html create mode 100644 examples/acceptance/multiclient-gmaps.html create mode 100644 examples/examples.json create mode 100644 examples/index.html create mode 100644 examples/internal/basemap-selector.html create mode 100644 examples/internal/gmaps-geometry-editor.html create mode 100644 examples/internal/leaflet-geometry-editor.html create mode 100644 examples/internal/leaflet-vector.html create mode 100644 examples/internal/search-geocode.html create mode 100644 examples/internal/viz/000.json create mode 100644 examples/internal/viz/001.json create mode 100644 examples/internal/viz/002.json create mode 100644 examples/internal/viz/003.json create mode 100644 examples/internal/vizjson.html create mode 100644 examples/public/dataviews/category.html create mode 100644 examples/public/dataviews/error-handling.html create mode 100644 examples/public/dataviews/formula.html create mode 100644 examples/public/dataviews/histogram.html create mode 100644 examples/public/dataviews/time-series.html create mode 100644 examples/public/filters/and-filter.html create mode 100644 examples/public/filters/bounding-box-gmaps.html create mode 100644 examples/public/filters/bounding-box-leaflet.html create mode 100644 examples/public/filters/category-filter.html create mode 100644 examples/public/filters/circle-filter.html create mode 100644 examples/public/filters/complex-filter.html create mode 100644 examples/public/filters/custom-bounding-box-leaflet.html create mode 100644 examples/public/filters/polygon-filter.html create mode 100644 examples/public/filters/range-filter.html create mode 100644 examples/public/layers/change-feature-columns-gmaps.html create mode 100644 examples/public/layers/change-feature-columns-leaflet.html create mode 100644 examples/public/layers/change-order-gmaps.html create mode 100644 examples/public/layers/change-order-leaflet.html create mode 100644 examples/public/layers/change-source-gmaps.html create mode 100644 examples/public/layers/change-source-leaflet.html create mode 100644 examples/public/layers/change-style-gmaps.html create mode 100644 examples/public/layers/change-style-leaflet.html create mode 100644 examples/public/layers/feature-click-gmaps.html create mode 100644 examples/public/layers/feature-click-leaflet.html create mode 100644 examples/public/layers/feature-over-out-gmaps.html create mode 100644 examples/public/layers/feature-over-out-leaflet.html create mode 100644 examples/public/layers/layer-with-aggregation-cluster-gmaps.html create mode 100644 examples/public/layers/layer-with-aggregation-cluster-leaflet.html create mode 100644 examples/public/layers/layer-with-aggregation-gmaps.html create mode 100644 examples/public/layers/layer-with-aggregation-leaflet.html create mode 100644 examples/public/layers/layer-with-zoom-options-gmaps.html create mode 100644 examples/public/layers/layer-with-zoom-options-leaflet.html create mode 100644 examples/public/layers/multilayer-gmaps.html create mode 100644 examples/public/layers/multilayer-leaflet.html create mode 100644 examples/public/layers/single-layer-gmaps.html create mode 100644 examples/public/layers/single-layer-leaflet.html create mode 100644 examples/public/misc/boilerplate-gmaps.html create mode 100644 examples/public/misc/boilerplate-leaflet.html create mode 100644 examples/public/misc/custom-search-dataset-gmaps.html create mode 100644 examples/public/misc/custom-search-dataset-leaflet.html create mode 100644 examples/public/misc/edit-sql-cartocss-gmaps.html create mode 100644 examples/public/misc/edit-sql-cartocss-leaflet.html create mode 100644 examples/public/misc/error-handling-gmaps.html create mode 100644 examples/public/misc/error-handling-leaflet.html create mode 100644 examples/public/misc/filter-data-map-gmaps.html create mode 100644 examples/public/misc/filter-data-map-leaflet.html create mode 100644 examples/public/misc/guide-gmaps.html create mode 100644 examples/public/misc/guide-leaflet.html create mode 100644 examples/public/misc/hexagon-aggregation-gmaps.html create mode 100644 examples/public/misc/hexagon-aggregation-leaflet.html create mode 100644 examples/public/misc/legends-gmaps.html create mode 100644 examples/public/misc/legends-leaflet.html create mode 100644 examples/public/misc/piechart-vega.html create mode 100644 examples/public/misc/populated-places-gmaps.html create mode 100644 examples/public/misc/populated-places-leaflet.html create mode 100644 examples/public/misc/popups-gmaps.html create mode 100644 examples/public/misc/popups-leaflet.html create mode 100644 examples/public/misc/popups-video-gmaps.html create mode 100644 examples/public/misc/popups-video-leaflet.html create mode 100644 examples/public/misc/storymap-leaflet.html create mode 100644 examples/public/style.css create mode 100644 grunt/README.md create mode 100644 grunt/tasks/_browserify-bundles.js create mode 100644 grunt/tasks/browserify.js create mode 100644 grunt/tasks/clean.js create mode 100644 grunt/tasks/concat.js create mode 100644 grunt/tasks/connect.js create mode 100644 grunt/tasks/copy.js create mode 100644 grunt/tasks/cssmin.js create mode 100644 grunt/tasks/exorcise.js create mode 100644 grunt/tasks/fastly.js create mode 100644 grunt/tasks/imagemin.js create mode 100644 grunt/tasks/jasmine.js create mode 100644 grunt/tasks/s3.js create mode 100644 grunt/tasks/scss.js create mode 100644 grunt/tasks/terser.js create mode 100644 grunt/tasks/usebanner.js create mode 100644 grunt/tasks/watch.js create mode 100644 gulpfile.js create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 scripts/deploy-docs-examples.sh create mode 100644 scripts/generate-package-json.js create mode 100644 scripts/package.public.json create mode 100755 scripts/release.sh create mode 100644 secrets.example.json create mode 100644 security.txt create mode 100644 src/analysis/analysis-model.js create mode 100644 src/analysis/analysis-poller.js create mode 100644 src/analysis/analysis-service.js create mode 100644 src/analysis/camshaft-reference.js create mode 100644 src/api/create-vis.js create mode 100644 src/api/promise.js create mode 100644 src/api/sql.js create mode 100644 src/api/v4/README.md create mode 100644 src/api/v4/client.js create mode 100644 src/api/v4/constants.js create mode 100644 src/api/v4/dataview/base.js create mode 100644 src/api/v4/dataview/category/index.js create mode 100644 src/api/v4/dataview/category/parse-data.js create mode 100644 src/api/v4/dataview/formula/index.js create mode 100644 src/api/v4/dataview/formula/parse-data.js create mode 100644 src/api/v4/dataview/histogram/index.js create mode 100644 src/api/v4/dataview/histogram/parse-data.js create mode 100644 src/api/v4/dataview/index.js create mode 100644 src/api/v4/dataview/time-series/index.js create mode 100644 src/api/v4/dataview/time-series/parse-data.js create mode 100644 src/api/v4/error-handling/carto-error-extender.js create mode 100644 src/api/v4/error-handling/carto-error.js create mode 100644 src/api/v4/error-handling/carto-validation-error.js create mode 100644 src/api/v4/error-handling/error-list/ajax-errors.js create mode 100644 src/api/v4/error-handling/error-list/index.js create mode 100644 src/api/v4/error-handling/error-list/validation-errors.js create mode 100644 src/api/v4/error-handling/error-list/windshaft-errors.js create mode 100644 src/api/v4/error-handling/error-tracker.js create mode 100644 src/api/v4/events.js create mode 100644 src/api/v4/filter/and.js create mode 100644 src/api/v4/filter/base-sql.js create mode 100644 src/api/v4/filter/base.js create mode 100644 src/api/v4/filter/bounding-box-gmaps.js create mode 100644 src/api/v4/filter/bounding-box-leaflet.js create mode 100644 src/api/v4/filter/bounding-box.js create mode 100644 src/api/v4/filter/category.js create mode 100644 src/api/v4/filter/circle.js create mode 100644 src/api/v4/filter/filters-collection.js create mode 100644 src/api/v4/filter/index.js create mode 100644 src/api/v4/filter/or.js create mode 100644 src/api/v4/filter/polygon.js create mode 100644 src/api/v4/filter/range.js create mode 100644 src/api/v4/filter/spatial-filter-types.js create mode 100644 src/api/v4/index.js create mode 100644 src/api/v4/layer/aggregation.js create mode 100644 src/api/v4/layer/base.js create mode 100644 src/api/v4/layer/event-types.js create mode 100644 src/api/v4/layer/index.js create mode 100644 src/api/v4/layer/layer.js create mode 100644 src/api/v4/layer/metadata/base.js create mode 100644 src/api/v4/layer/metadata/buckets.js create mode 100644 src/api/v4/layer/metadata/categories.js create mode 100644 src/api/v4/layer/metadata/parser.js create mode 100644 src/api/v4/layers.js create mode 100644 src/api/v4/native/google-maps-map-type.js create mode 100644 src/api/v4/native/leaflet-layer.js create mode 100644 src/api/v4/native/trigger-layer-feature-event.js create mode 100644 src/api/v4/source/base.js create mode 100644 src/api/v4/source/dataset.js create mode 100644 src/api/v4/source/index.js create mode 100644 src/api/v4/source/sql.js create mode 100644 src/api/v4/style/base.js create mode 100644 src/api/v4/style/cartocss.js create mode 100644 src/api/v4/style/index.js create mode 100644 src/api/vizjson.js create mode 100644 src/cartodb.js create mode 100644 src/cdb.config.js create mode 100644 src/cdb.js create mode 100644 src/cdb.log.js create mode 100644 src/cdb.templates.js create mode 100644 src/constants.js create mode 100644 src/core/config.js create mode 100644 src/core/loader.js create mode 100644 src/core/model.js create mode 100644 src/core/profiler.js create mode 100644 src/core/sanitize.js create mode 100644 src/core/template-list.js create mode 100644 src/core/template.js create mode 100644 src/core/util.js create mode 100644 src/core/view.js create mode 100644 src/dataviews/category-dataview-model.js create mode 100644 src/dataviews/category-dataview/categories-collection.js create mode 100644 src/dataviews/category-dataview/category-item-model.js create mode 100644 src/dataviews/category-dataview/category-model-range.js create mode 100644 src/dataviews/category-dataview/search-model.js create mode 100644 src/dataviews/dataview-model-base.js create mode 100644 src/dataviews/dataviews-collection.js create mode 100644 src/dataviews/dataviews-factory.js create mode 100644 src/dataviews/formula-dataview-model.js create mode 100644 src/dataviews/helpers/histogram-helper.js create mode 100644 src/dataviews/histogram-dataview-model.js create mode 100644 src/dataviews/histogram-dataview/histogram-data-model.js create mode 100644 src/engine.js create mode 100644 src/geo/adapters/gmaps-bounding-box-adapter.js create mode 100644 src/geo/adapters/leaflet-bounding-box-adapter.js create mode 100644 src/geo/adapters/map-model-bounding-box-adapter.js create mode 100644 src/geo/cartodb-layer-group-view-base.js create mode 100644 src/geo/cartodb-layer-group.js create mode 100644 src/geo/geocoder/mapbox-geocoder.js create mode 100644 src/geo/geocoder/tomtom-geocoder.js create mode 100644 src/geo/geometry-models/geojson-helper.js create mode 100644 src/geo/geometry-models/geometry-base.js create mode 100644 src/geo/geometry-models/geometry-factory.js create mode 100644 src/geo/geometry-models/multi-geometry-base.js create mode 100644 src/geo/geometry-models/multi-point.js create mode 100644 src/geo/geometry-models/multi-polygon.js create mode 100644 src/geo/geometry-models/multi-polyline.js create mode 100644 src/geo/geometry-models/path-base.js create mode 100644 src/geo/geometry-models/point.js create mode 100644 src/geo/geometry-models/polygon.js create mode 100644 src/geo/geometry-models/polyline.js create mode 100644 src/geo/geometry-views/base/geometry-view-base.js create mode 100644 src/geo/geometry-views/base/marker-adapter-base.js create mode 100644 src/geo/geometry-views/base/multi-geometry-view-base.js create mode 100644 src/geo/geometry-views/base/path-adapter-base.js create mode 100644 src/geo/geometry-views/base/path-view-base.js create mode 100644 src/geo/geometry-views/base/point-view-base.js create mode 100644 src/geo/geometry-views/geometry-view-factory.js create mode 100644 src/geo/geometry-views/gmaps/gmaps-coordinates.js create mode 100644 src/geo/geometry-views/gmaps/gmaps-marker-adapter.js create mode 100644 src/geo/geometry-views/gmaps/gmaps-path-adapter.js create mode 100644 src/geo/geometry-views/gmaps/multi-point-view.js create mode 100644 src/geo/geometry-views/gmaps/multi-polygon-view.js create mode 100644 src/geo/geometry-views/gmaps/multi-polyline-view.js create mode 100644 src/geo/geometry-views/gmaps/point-view.js create mode 100644 src/geo/geometry-views/gmaps/polygon-view.js create mode 100644 src/geo/geometry-views/gmaps/polyline-view.js create mode 100644 src/geo/geometry-views/leaflet/leaflet-marker-adapter.js create mode 100644 src/geo/geometry-views/leaflet/leaflet-path-adapter.js create mode 100644 src/geo/geometry-views/leaflet/multi-point-view.js create mode 100644 src/geo/geometry-views/leaflet/multi-polygon-view.js create mode 100644 src/geo/geometry-views/leaflet/multi-polyline-view.js create mode 100644 src/geo/geometry-views/leaflet/point-view.js create mode 100644 src/geo/geometry-views/leaflet/polygon-view.js create mode 100644 src/geo/geometry-views/leaflet/polyline-view.js create mode 100644 src/geo/gmaps/gmaps-base-layer-view.js create mode 100644 src/geo/gmaps/gmaps-cartodb-layer-group-view.js create mode 100644 src/geo/gmaps/gmaps-layer-view-factory.js create mode 100644 src/geo/gmaps/gmaps-layer-view.js create mode 100644 src/geo/gmaps/gmaps-map-view.js create mode 100644 src/geo/gmaps/gmaps-plain-layer-view.js create mode 100644 src/geo/gmaps/gmaps-tiled-layer-view.js create mode 100644 src/geo/gmaps/gmaps-torque-layer-view.js create mode 100644 src/geo/gmaps/projector.js create mode 100644 src/geo/leaflet/leaflet-cartodb-layer-group-view.js create mode 100644 src/geo/leaflet/leaflet-layer-view-factory.js create mode 100644 src/geo/leaflet/leaflet-layer-view.js create mode 100644 src/geo/leaflet/leaflet-map-view.js create mode 100644 src/geo/leaflet/leaflet-plain-layer-view.js create mode 100644 src/geo/leaflet/leaflet-tiled-layer-view.js create mode 100644 src/geo/leaflet/leaflet-torque-layer-view.js create mode 100644 src/geo/leaflet/leaflet-wms-layer-view.js create mode 100644 src/geo/map-view-factory.js create mode 100644 src/geo/map-view.js create mode 100644 src/geo/map.js create mode 100644 src/geo/map/cartodb-layer.js create mode 100644 src/geo/map/gmaps-base-layer.js create mode 100644 src/geo/map/infowindow-template.js create mode 100644 src/geo/map/layer-model-base.js create mode 100644 src/geo/map/layer-types.js create mode 100644 src/geo/map/layers.js create mode 100644 src/geo/map/legends/bubble-legend-model.js create mode 100644 src/geo/map/legends/category-legend-model.js create mode 100644 src/geo/map/legends/choropleth-legend-model.js create mode 100644 src/geo/map/legends/custom-choropleth-legend-model.js create mode 100644 src/geo/map/legends/custom-legend-model.js create mode 100644 src/geo/map/legends/legend-model-base.js create mode 100644 src/geo/map/legends/legends.js create mode 100644 src/geo/map/legends/static-legend-model-base.js create mode 100644 src/geo/map/legends/torque-legend-model.js create mode 100644 src/geo/map/plain-layer.js create mode 100644 src/geo/map/popup-fields.js create mode 100644 src/geo/map/tile-layer.js create mode 100644 src/geo/map/tooltip-template.js create mode 100644 src/geo/map/torque-layer.js create mode 100644 src/geo/map/wms-layer.js create mode 100644 src/geo/render-modes.js create mode 100644 src/geo/torque-layer-view-base.js create mode 100644 src/geo/ui/attribution/attribution-template.tpl create mode 100644 src/geo/ui/attribution/attribution-view.js create mode 100644 src/geo/ui/default-infowindow-template.tpl create mode 100644 src/geo/ui/infowindow-model.js create mode 100644 src/geo/ui/infowindow-view.js create mode 100644 src/geo/ui/legends/base/img-loader-view.js create mode 100644 src/geo/ui/legends/base/legend-title.tpl create mode 100644 src/geo/ui/legends/base/legend-view-base.js create mode 100644 src/geo/ui/legends/bubble/legend-template.tpl create mode 100644 src/geo/ui/legends/bubble/legend-view.js create mode 100644 src/geo/ui/legends/bubble/placeholder-template.tpl create mode 100644 src/geo/ui/legends/categories/legend-template.tpl create mode 100644 src/geo/ui/legends/categories/legend-view.js create mode 100644 src/geo/ui/legends/categories/placeholder-template.tpl create mode 100644 src/geo/ui/legends/choropleth/legend-template.tpl create mode 100644 src/geo/ui/legends/choropleth/legend-view.js create mode 100644 src/geo/ui/legends/choropleth/placeholder-template.tpl create mode 100644 src/geo/ui/legends/custom-choropleth/legend-template.tpl create mode 100644 src/geo/ui/legends/custom-choropleth/legend-view.js create mode 100644 src/geo/ui/legends/custom/legend-template.tpl create mode 100644 src/geo/ui/legends/custom/legend-view.js create mode 100644 src/geo/ui/legends/layer-legends-template.tpl create mode 100644 src/geo/ui/legends/layer-legends-view.js create mode 100644 src/geo/ui/legends/legend-view-factory.js create mode 100644 src/geo/ui/legends/legends-view.js create mode 100644 src/geo/ui/legends/legends-view.tpl create mode 100644 src/geo/ui/limits/limits-view.js create mode 100644 src/geo/ui/logo-view.js create mode 100644 src/geo/ui/logo.tpl create mode 100644 src/geo/ui/overlays-container.tpl create mode 100644 src/geo/ui/overlays-view.js create mode 100644 src/geo/ui/search/search.js create mode 100644 src/geo/ui/search/search_infowindow_template.tpl create mode 100644 src/geo/ui/search/search_template.tpl create mode 100644 src/geo/ui/tiles-loader.js create mode 100644 src/geo/ui/tiles/tiles-template.tpl create mode 100644 src/geo/ui/tiles/tiles-view.js create mode 100644 src/geo/ui/tooltip-model.js create mode 100644 src/geo/ui/tooltip-view.js create mode 100644 src/geo/ui/zoom/zoom-template.tpl create mode 100644 src/geo/ui/zoom/zoom-view.js create mode 100644 src/index.js create mode 100644 src/ui/common/fullscreen/fullscreen-template.tpl create mode 100644 src/ui/common/fullscreen/fullscreen-view.js create mode 100644 src/util/backbone-abort-sync.js create mode 100644 src/util/date-utils.js create mode 100644 src/util/formatter.js create mode 100644 src/util/get-object-value.js create mode 100644 src/util/tile-error.tpl create mode 100644 src/vis/dataviews-tracker.js create mode 100644 src/vis/drawing-controller.js create mode 100644 src/vis/edition-controller.js create mode 100644 src/vis/geometry-management-controller.js create mode 100644 src/vis/image/queue.js create mode 100644 src/vis/infowindow-manager.js create mode 100644 src/vis/layers-factory.js create mode 100644 src/vis/map-cursor-manager.js create mode 100644 src/vis/map-events-manager.js create mode 100644 src/vis/overlays-factory.js create mode 100644 src/vis/overlays/attribution.js create mode 100644 src/vis/overlays/custom.js create mode 100644 src/vis/overlays/fullscreen.js create mode 100644 src/vis/overlays/index.js create mode 100644 src/vis/overlays/layer_selector.js create mode 100644 src/vis/overlays/limits.js create mode 100644 src/vis/overlays/loader.js create mode 100644 src/vis/overlays/logo.js create mode 100644 src/vis/overlays/search.js create mode 100644 src/vis/overlays/tiles.js create mode 100644 src/vis/overlays/zoom.js create mode 100644 src/vis/settings.js create mode 100644 src/vis/tooltip-manager.js create mode 100644 src/vis/vis-view.js create mode 100644 src/vis/vis.js create mode 100644 src/vis/vis/infowindow-template.js create mode 100644 src/windshaft-integration/legends/rule-to-bubble-legend-adapter.js create mode 100644 src/windshaft-integration/legends/rule-to-category-legend-adapter.js create mode 100644 src/windshaft-integration/legends/rule-to-choropleth-legend-adapter.js create mode 100644 src/windshaft-integration/legends/rule-to-legend-model-adapters.js create mode 100644 src/windshaft-integration/legends/rule.js create mode 100644 src/windshaft-integration/model-updater.js create mode 100644 src/windshaft/client.js create mode 100644 src/windshaft/config.js create mode 100644 src/windshaft/error-parser.js create mode 100644 src/windshaft/error.js create mode 100644 src/windshaft/filters/base.js create mode 100644 src/windshaft/filters/bounding-box.js create mode 100644 src/windshaft/filters/category.js create mode 100644 src/windshaft/filters/circle.js create mode 100644 src/windshaft/filters/polygon.js create mode 100644 src/windshaft/filters/range.js create mode 100644 src/windshaft/map-serializer/anonymous-map-serializer/analysis-serializer.js create mode 100644 src/windshaft/map-serializer/anonymous-map-serializer/anonymous-map-serializer.js create mode 100644 src/windshaft/map-serializer/anonymous-map-serializer/dataviews-serializer.js create mode 100644 src/windshaft/map-serializer/anonymous-map-serializer/layers-serializer.js create mode 100644 src/windshaft/map-serializer/named-map-serializer/named-map-serializer.js create mode 100644 src/windshaft/request-tracker.js create mode 100644 src/windshaft/request.js create mode 100644 src/windshaft/response.js create mode 100644 test/fail-tests-if-have-errors-in-src.js create mode 100644 test/helpers/SpecHelper.js create mode 100644 test/helpers/mockFactory.js create mode 100644 test/install-source-map-support.js create mode 100644 test/spec/analysis/analysis-model.spec.js create mode 100644 test/spec/analysis/analysis-poller.spec.js create mode 100644 test/spec/analysis/analysis-service.spec.js create mode 100644 test/spec/analysis/camshaft-reference.spec.js create mode 100644 test/spec/api/createVis/create-vis.spec.js create mode 100644 test/spec/api/createVis/scenarios/basic_vis.json.js create mode 100644 test/spec/api/createVis/scenarios/index.js create mode 100644 test/spec/api/sql.spec.js create mode 100644 test/spec/api/v4/client.spec.js create mode 100644 test/spec/api/v4/dataview/base.spec.js create mode 100644 test/spec/api/v4/dataview/category.spec.js create mode 100644 test/spec/api/v4/dataview/formula.spec.js create mode 100644 test/spec/api/v4/dataview/histogram.spec.js create mode 100644 test/spec/api/v4/dataview/time-series.spec.js create mode 100644 test/spec/api/v4/error-handling/carto-error-extender.spec.js create mode 100644 test/spec/api/v4/error-handling/carto-error.spec.js create mode 100644 test/spec/api/v4/error-handling/carto-validation-error.spec.js create mode 100644 test/spec/api/v4/filter/base-sql.spec.js create mode 100644 test/spec/api/v4/filter/bounding-box-gmaps.spec.js create mode 100644 test/spec/api/v4/filter/bounding-box-leaflet.spec.js create mode 100644 test/spec/api/v4/filter/bounding-box.spec.js create mode 100644 test/spec/api/v4/filter/category.spec.js create mode 100644 test/spec/api/v4/filter/circle.spec.js create mode 100644 test/spec/api/v4/filter/filters-collection.spec.js create mode 100644 test/spec/api/v4/filter/polygon.spec.js create mode 100644 test/spec/api/v4/filter/range.spec.js create mode 100644 test/spec/api/v4/layer/aggregation.spec.js create mode 100644 test/spec/api/v4/layer/layer.spec.js create mode 100644 test/spec/api/v4/layer/metadata/parser.spec.js create mode 100644 test/spec/api/v4/layers.spec.js create mode 100644 test/spec/api/v4/native/google-maps-map-type.spec.js create mode 100644 test/spec/api/v4/native/leaflet-layer.spec.js create mode 100644 test/spec/api/v4/source/dataset.spec.js create mode 100644 test/spec/api/v4/source/sql.spec.js create mode 100644 test/spec/api/v4/style/cartocss.spec.js create mode 100644 test/spec/api/vizjson.spec.js create mode 100644 test/spec/cartodb.mod.torque.spec.js create mode 100644 test/spec/cartodb.spec.js create mode 100644 test/spec/core/model.spec.js create mode 100644 test/spec/core/sanitize.spec.js create mode 100644 test/spec/core/template-list.spec.js create mode 100644 test/spec/core/template.spec.js create mode 100644 test/spec/core/util.spec.js create mode 100644 test/spec/core/view.spec.js create mode 100644 test/spec/dataviews/category-dataview-model.spec.js create mode 100644 test/spec/dataviews/category-dataview/categories-collection.spec.js create mode 100644 test/spec/dataviews/dataview-model-base.spec.js create mode 100644 test/spec/dataviews/dataviews-collection.spec.js create mode 100644 test/spec/dataviews/dataviews-factory.spec.js create mode 100644 test/spec/dataviews/formula-dataview-model.spec.js create mode 100644 test/spec/dataviews/helpers/histogram-helper.js create mode 100644 test/spec/dataviews/histogram-dataview-model.spec.js create mode 100644 test/spec/dataviews/histogram-dataview/histogram-data-model.spec.js create mode 100644 test/spec/engine.spec.js create mode 100644 test/spec/fixtures/engine.fixture.js create mode 100644 test/spec/geo/cartodb-layer-group.spec.js create mode 100644 test/spec/geo/geocoder/mapbox-geocoder-response-0.json create mode 100644 test/spec/geo/geocoder/mapbox-geocoder-response-1.json create mode 100644 test/spec/geo/geocoder/mapbox-geocoder.spec.js create mode 100644 test/spec/geo/geocoder/tomtom-geocoder-response-0.json create mode 100644 test/spec/geo/geocoder/tomtom-geocoder.spec.js create mode 100644 test/spec/geo/geometry-models/geometry-factory.spec.js create mode 100644 test/spec/geo/geometry-models/multi-point.spec.js create mode 100644 test/spec/geo/geometry-models/multi-polygon.spec.js create mode 100644 test/spec/geo/geometry-models/multi-polyline.spec.js create mode 100644 test/spec/geo/geometry-models/point.spec.js create mode 100644 test/spec/geo/geometry-models/polygon.spec.js create mode 100644 test/spec/geo/geometry-models/polyline.spec.js create mode 100644 test/spec/geo/geometry-views/coordinates-comparator.js create mode 100644 test/spec/geo/geometry-views/create-map-view.js create mode 100644 test/spec/geo/geometry-views/gmaps/multi-point-view.spec.js create mode 100644 test/spec/geo/geometry-views/gmaps/multi-polygon-view.spec.js create mode 100644 test/spec/geo/geometry-views/gmaps/multi-polyline-view.spec.js create mode 100644 test/spec/geo/geometry-views/gmaps/point-view.spec.js create mode 100644 test/spec/geo/geometry-views/gmaps/polygon-view.spec.js create mode 100644 test/spec/geo/geometry-views/gmaps/polyline-view.spec.js create mode 100644 test/spec/geo/geometry-views/leaflet/multi-point-view.spec.js create mode 100644 test/spec/geo/geometry-views/leaflet/multi-polygon-view.spec.js create mode 100644 test/spec/geo/geometry-views/leaflet/multi-polyline-view.spec.js create mode 100644 test/spec/geo/geometry-views/leaflet/point-view.spec.js create mode 100644 test/spec/geo/geometry-views/leaflet/polygon-view.spec.js create mode 100644 test/spec/geo/geometry-views/leaflet/polyline-view.spec.js create mode 100644 test/spec/geo/geometry-views/shared-tests-for-multi-geometry-views.js create mode 100644 test/spec/geo/geometry-views/shared-tests-for-multi-point-views.js create mode 100644 test/spec/geo/geometry-views/shared-tests-for-multi-polygon-views.js create mode 100644 test/spec/geo/geometry-views/shared-tests-for-multi-polyline-views.js create mode 100644 test/spec/geo/geometry-views/shared-tests-for-path-views.js create mode 100644 test/spec/geo/geometry-views/shared-tests-for-point-views.js create mode 100644 test/spec/geo/geometry-views/shared-tests-for-polygon-views.js create mode 100644 test/spec/geo/geometry-views/shared-tests-for-polyline-views.js create mode 100644 test/spec/geo/gmaps/gmaps-cartodb-layer-group.spec.js create mode 100644 test/spec/geo/gmaps/gmaps-map-view.spec.js create mode 100644 test/spec/geo/gmaps/gmaps-torque-layer-view.spec.js create mode 100644 test/spec/geo/gmaps_cartodb_layer/hide.js create mode 100644 test/spec/geo/gmaps_cartodb_layer/interaction.js create mode 100644 test/spec/geo/gmaps_cartodb_layer/opacity.js create mode 100644 test/spec/geo/gmaps_cartodb_layer/show.js create mode 100644 test/spec/geo/leaflet/leaflet-cartodb-layer-group-view.spec.js create mode 100644 test/spec/geo/leaflet/leaflet-layer-view.spec.js create mode 100644 test/spec/geo/leaflet/leaflet-map-view.spec.js create mode 100644 test/spec/geo/leaflet/leaflet-tiled-layer-view.spec.js create mode 100644 test/spec/geo/leaflet/leaflet-torque-layer-view.spec.js create mode 100644 test/spec/geo/map-view.spec.js create mode 100644 test/spec/geo/map.spec.js create mode 100644 test/spec/geo/map/cartodb-layer.spec.js create mode 100644 test/spec/geo/map/gmaps-base-layer.spec.js create mode 100644 test/spec/geo/map/infowindow-template.spec.js create mode 100644 test/spec/geo/map/layer-model-base.spec.js create mode 100644 test/spec/geo/map/layers.spec.js create mode 100644 test/spec/geo/map/legends/legend-model-base.spec.js create mode 100644 test/spec/geo/map/legends/legends.spec.js create mode 100644 test/spec/geo/map/legends/static-legend-model.spec.js create mode 100644 test/spec/geo/map/plain-layer.spec.js create mode 100644 test/spec/geo/map/shared-for-interactive-layers.js create mode 100644 test/spec/geo/map/tile-layer.spec.js create mode 100644 test/spec/geo/map/tooltip-template.spec.js create mode 100644 test/spec/geo/map/torque-layer.spec.js create mode 100644 test/spec/geo/shared-tests-for-carto-layer-group.js create mode 100644 test/spec/geo/shared-tests-for-torque-layer.js create mode 100644 test/spec/geo/torque-layer-view-base.spec.js create mode 100644 test/spec/geo/ui/attribution-view.spec.js create mode 100644 test/spec/geo/ui/infowindow-model.spec.js create mode 100644 test/spec/geo/ui/infowindow-view.spec.js create mode 100644 test/spec/geo/ui/legends/base/legend-view-base.spec.js create mode 100644 test/spec/geo/ui/legends/bubble/legend-view.spec.js create mode 100644 test/spec/geo/ui/legends/choropleth/custom-legend-view.spec.js create mode 100644 test/spec/geo/ui/legends/choropleth/legend-view.spec.js create mode 100644 test/spec/geo/ui/legends/custom/img-loader-view.spec.js create mode 100644 test/spec/geo/ui/legends/custom/legend-view.spec.js create mode 100644 test/spec/geo/ui/legends/layer-legends-view.spec.js create mode 100644 test/spec/geo/ui/legends/legends-view.spec.js create mode 100644 test/spec/geo/ui/limits-view.spec.js create mode 100644 test/spec/geo/ui/overlays-view.spec.js create mode 100644 test/spec/geo/ui/search.spec.js create mode 100644 test/spec/geo/ui/tooltip.spec.js create mode 100644 test/spec/geo/ui/zoom.spec.js create mode 100644 test/spec/util/backbone-abort-sync.spec.js create mode 100644 test/spec/util/formatter.spec.js create mode 100644 test/spec/vis/dataviews-tracker.spec.js create mode 100644 test/spec/vis/infowindow-manager.spec.js create mode 100644 test/spec/vis/layers-factory.spec.js create mode 100644 test/spec/vis/map-cursor-manager.spec.js create mode 100644 test/spec/vis/overlays-factory.spec.js create mode 100644 test/spec/vis/tooltip-manager.spec.js create mode 100644 test/spec/vis/vis-view.spec.js create mode 100644 test/spec/vis/vis.spec.js create mode 100644 test/spec/windshaft-integration/legends/rule-to-bubble-legend-adapter.spec.js create mode 100644 test/spec/windshaft-integration/legends/rule-to-category-legend-adapter.spec.js create mode 100644 test/spec/windshaft-integration/legends/rule-to-choropleth-legend-adapter.spec.js create mode 100644 test/spec/windshaft-integration/model-updater.spec.js create mode 100644 test/spec/windshaft/client.spec.js create mode 100644 test/spec/windshaft/error-parser.spec.js create mode 100644 test/spec/windshaft/error.mock.js create mode 100644 test/spec/windshaft/error.spec.js create mode 100644 test/spec/windshaft/filters/base.spec.js create mode 100644 test/spec/windshaft/filters/category.spec.js create mode 100644 test/spec/windshaft/filters/range.spec.js create mode 100644 test/spec/windshaft/map-serializer/anonymous-map-serializer/analysis-serializer.spec.js create mode 100644 test/spec/windshaft/map-serializer/anonymous-map-serializer/anonymous-map-serializer.spec.js create mode 100644 test/spec/windshaft/map-serializer/anonymous-map-serializer/dataviews-serializer.spec.js create mode 100644 test/spec/windshaft/map-serializer/anonymous-map-serializer/layers-serializer.spec.js create mode 100644 test/spec/windshaft/map-serializer/named-map-serializer/named-map-serializer.spec.js create mode 100644 test/spec/windshaft/request-tracker.spec.js create mode 100644 test/spec/windshaft/response.mock.js create mode 100644 test/spec/windshaft/response.spec.js create mode 100644 themes/img/bg-attribution-button.png create mode 100644 themes/img/bubbles.png create mode 100644 themes/img/burguer.png create mode 100644 themes/img/burguer@2x.png create mode 100644 themes/img/dark.png create mode 100644 themes/img/dark_bubbles.png create mode 100644 themes/img/dark_loader.gif create mode 100644 themes/img/dark_loader@2x.gif create mode 100644 themes/img/default-marker-icon.png create mode 100644 themes/img/facebook.png create mode 100644 themes/img/fullscreen.png create mode 100644 themes/img/fullscreen_mobile.png create mode 100644 themes/img/handle.png create mode 100644 themes/img/handle@2x.png create mode 100644 themes/img/headers.png create mode 100644 themes/img/icon-save.svg create mode 100644 themes/img/icon-share.svg create mode 100644 themes/img/image_not_found.png create mode 100755 themes/img/layers-2x.png create mode 100755 themes/img/layers.png create mode 100644 themes/img/light.png create mode 100644 themes/img/link.png create mode 100644 themes/img/loader.gif create mode 100644 themes/img/loader@2x.gif create mode 100644 themes/img/mobile_zoom.png create mode 100644 themes/img/other.png create mode 100644 themes/img/other@2x.png create mode 100644 themes/img/shadow.png create mode 100644 themes/img/share.png create mode 100644 themes/img/share@2x.png create mode 100644 themes/img/slide_left.png create mode 100644 themes/img/slide_left@2x.png create mode 100644 themes/img/slide_right.png create mode 100644 themes/img/slide_right@2x.png create mode 100644 themes/img/slider.png create mode 100644 themes/img/toggle_aside.png create mode 100644 themes/img/twitter.png create mode 100644 themes/scss/entry.scss create mode 100644 themes/scss/infowindow/_cartodb-infowindow-default.scss create mode 100644 themes/scss/infowindow/_cartodb-infowindow-legacy.scss create mode 100644 themes/scss/infowindow/themes/_custom.scss create mode 100644 themes/scss/infowindow/themes/_dark.scss create mode 100644 themes/scss/infowindow/themes/_light.scss create mode 100644 themes/scss/map/_attributions.scss create mode 100644 themes/scss/map/_cartodb-map-light.scss create mode 100644 themes/scss/map/_limits.scss create mode 100644 themes/scss/map/_map.scss create mode 100644 themes/scss/map/_overlays.scss create mode 100644 themes/scss/tooltip/_cartodb-tooltip-default.scss create mode 100644 themes/scss/tooltip/themes/_dark.scss create mode 100644 themes/scss/tooltip/themes/_light.scss create mode 100755 themes/scss/vendor/_leaflet.scss create mode 100644 vendor/GeoJSON.js create mode 100644 vendor/backbone.js create mode 100644 vendor/html-css-sanitizer-bundle.js create mode 100644 vendor/jquery.min.js create mode 100644 vendor/json2.js create mode 100644 vendor/lzma.js create mode 100644 vendor/mod/carto.js create mode 100644 vendor/mousewheel.js create mode 100644 vendor/mustache.js create mode 100644 vendor/mwheelIntent.js create mode 100644 webpack/banner.js create mode 100644 webpack/webpack.config.js create mode 100644 webpack/webpack.min.config.js diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..0d1e358 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true + +# Javascript and json files are formated with 2 spaces with ut8 encoding +[*.{js,json}] +charset = utf-8 +indent_style = space +indent_size = 2 diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..e0cc02a --- /dev/null +++ b/.eslintignore @@ -0,0 +1,7 @@ +/dist +/doc +/docs +/examples +/grunt +/themes +/vendor diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..a61b185 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,14 @@ +{ + "extends": "semistandard", + "env": { + "es6": true, + "browser": true, + "jasmine": true + }, + "globals": { + "__ENV__": true + }, + "rules": { + "no-useless-escape": "off" + } +} diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..5d3ac42 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,29 @@ +### Context +*Please explain here below what you were doing when the issue happened* + + + +### Steps to Reproduce +*Please break down here below all the needed steps to reproduce the issue* + +1. +2. +3. + +### Current Result +*Please describe here below the current result you got* + + + +### Expected result +*Please describe here below what should be the expected behaviour* + + +### Browser and version +*What internet browser (Chrome, Firefox, etc) and version was you using and version* + + + +### Additional info +*Please add any information of interest here below* + diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 0000000..ec12eb6 --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,53 @@ +# Configuration for probot-stale - https://github.com/probot/stale + +# Number of days of inactivity before an Issue or Pull Request becomes stale +daysUntilStale: 365 + +# Number of days of inactivity before a stale Issue or Pull Request is closed. +# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. +daysUntilClose: 14 + +# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable +# exemptLabels: +# - pinned +# - security +# - "[Status] Maybe Later" + +# Set to true to ignore issues in a project (defaults to false) +exemptProjects: false + +# Set to true to ignore issues in a milestone (defaults to false) +exemptMilestones: false + +# Label to use when marking as stale +staleLabel: stale + +# Comment to post when marking as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. + +# Comment to post when removing the stale label. +unmarkComment: false + +# Comment to post when closing a stale Issue or Pull Request. +closeComment: false + +# Limit the number of actions per hour, from 1-30. Default is 30 +limitPerRun: 10 + +# Limit to only `issues` or `pulls` +# only: issues + +# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls': +# pulls: +# daysUntilStale: 30 +# markComment: > +# This pull request has been automatically marked as stale because it has not had +# recent activity. It will be closed if no further activity occurs. Thank you +# for your contributions. + +# issues: +# exemptLabels: +# - confirmed \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..18135ac --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +.DS_store +.idea/ +.grunt +.rvmrc +.tmp +.vscode +*.swp +dist +docs/public +docs/internal +Gemfile.lock +node_modules +npm-debug.log +secrets.json +secrets.v3.json +secrets.v4.json +test/SpecRunner*.html +themes/css/all.css +themes/css/cartodb.ie.css +themes/css/cartodb.css +yarn.lock +.sass-cache \ No newline at end of file diff --git a/.hound.yml b/.hound.yml new file mode 100644 index 0000000..107a98d --- /dev/null +++ b/.hound.yml @@ -0,0 +1,5 @@ +scss: + enabled: true + config_file: .scss-lint.yml +javascript: + enabled: false diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..e92a3ea --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +message = "Bump version to %s" diff --git a/.scss-lint.yml b/.scss-lint.yml new file mode 100644 index 0000000..dcd34b4 --- /dev/null +++ b/.scss-lint.yml @@ -0,0 +1,184 @@ +scss_files: 'themes/scss/**/*.scss' +exclude: + - 'themes/scss/common/reset.scss' + - 'themes/scss/map/cartodb-map-light.scss' + - 'themes/scss/tooltip/*.scss' + - 'themes/scss/vendor/*.scss' + - 'vendor/**/*.scss' +linters: + BangFormat: + enabled: true + space_before_bang: true + space_after_bang: false + BemDepthf: + enabled: true + max_elements: 3 + BorderZero: + enabled: false + convention: zero + ColorKeyword: + enabled: true + severity: warning + ColorVariable: + enabled: false + severity: warning + Comment: + enabled: false + DebugStatement: + enabled: true + DeclarationOrder: + enabled: true + DisableLinterReason: + enabled: false + DuplicateProperty: + enabled: true + ElsePlacement: + enabled: true + style: same_line + EmptyLineBetweenBlocks: + enabled: false + EmptyRule: + enabled: true + FinalNewline: + enabled: true + present: true + HexLength: + enabled: true + style: short + HexNotation: + enabled: true + style: uppercase + HexValidation: + enabled: true + IdSelector: + enabled: true + ImportantRule: + enabled: true + ImportPath: + enabled: true + leading_underscore: false + filename_extension: false + Indentation: + enabled: true + allow_non_nested_indentation: false + character: space + width: 2 + severity: warning + LeadingZero: + enabled: true + style: include_zero + MergeableSelector: + enabled: true + force_nesting: false + NameFormat: + enabled: true + # Valid cases + # - hello + # - helloMates + # - hello-buddy + # - hello-buddyOne + convention: '([a-z]*)([A-Z]+[a-z]*)*-{0,2}([a-z]([a-z]*[A-Z]*[a-z]*))(\.[a-z]+-[a-z]*[A-Z]*[a-z]*)?(:.*)*' + NestingDepth: + enabled: true + max_depth: 3 + severity: warning + PlaceholderInExtend: + enabled: false + PropertyCount: + enabled: true + include_nested: false + max_properties: 18 + severity: warning + PropertySortOrder: + enabled: true + order: smacss + ignore_unspecified: true + severity: warning + separate_groups: false + PropertySpelling: + enabled: true + extra_properties: [] + PropertyUnits: + global: ['em', 'rem', '%', 'px', 's', 'ms', 'vh'] # Allow relative units globally + properties: + border: ['px'] + line-height: ['px', ''] + margin: ['em', 'px', '%'] + QualifyingElement: + enabled: true + allow_element_with_attribute: true + allow_element_with_class: false + allow_element_with_id: false + severity: warning + SelectorDepth: + enabled: true + max_depth: 3 + severity: warning + SelectorFormat: + enabled: true + # Valid cases + # - 0..100% + # - i | em | strong | div | span | ... + # - CDB-Logo + # - CDB-LogoWadus + # - CDB-Logo--cartofante + # - CDB-LogoWadus--cartofante + # - CDB-Logo--cartofantePleased + # - CDB-Logo--cartofanteURL + # - CDB-Logo-oh + # - CDB-Logo-oh:hover + # - CDB-Logo:after + # - CDB-Logo.is-state + convention: '([0-9]+%)|([a-z]+(:.*)*)|CDB-([A-Z]+[a-z]*)([A-Z]+[a-z]*)*-{0,2}([a-z]([a-z]*[A-Z]*[a-z]*))(\.[a-z]+-[a-z]*[A-Z]*[a-z]*)?(:.*)*' + ignored_types: ['id'] + Shorthand: + enabled: true + severity: warning + SingleLinePerProperty: + enabled: true + allow_single_line_rule_sets: false + SingleLinePerSelector: + enabled: true + SpaceAfterComma: + enabled: true + SpaceAfterPropertyColon: + enabled: true + style: one_space + SpaceAfterPropertyName: + enabled: true + SpaceAfterVariableName: + enabled: true + SpaceAroundOperator: + enabled: true + style: one_space + SpaceBeforeBrace: + enabled: true + style: space + allow_single_line_padding: false + SpaceBetweenParens: + enabled: true + spaces: 0 + StringQuotes: + enabled: true + style: single_quotes + TrailingSemicolon: + enabled: true + severity: warning + TrailingZero: + enabled: true + UnnecessaryMantissa: + enabled: true + UnnecessaryParentReference: + enabled: true + UrlFormat: + enabled: true + UrlQuotes: + enabled: true + VariableForProperty: + enabled: false + properties: [] + VendorPrefixes: + enabled: false + ZeroUnit: + enabled: true + severity: warning diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..24bf894 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,21 @@ +sudo: false +cache: false +language: node_js +node_js: + - 6.9.2 +install: + - npm install +before_script: + - cp secrets.example.json secrets.json + - npm install -g grunt-cli +script: + - npm test +after_success: + - bash scripts/deploy-docs-examples.sh +notifications: + email: + on_success: never + on_failure: change +env: + global: + secure: lcdjTpuv2imFSgcYWMLV4odyZ4jOTjJhyViGg9hOFzggoPGFAjplz6bPgNJsxq20ZsFDw4UN6UlZ8lPGlByIUBhf2OXs0cEJDcNSoYAV7CuTMSdZsLDb+TrWQvjtSZMBEkeVENfJjyfiJo7PCt2/cIQXSVSnhZ5Uiir+Eus5x/s= diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..37bd272 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,199 @@ +# Changelog + +CARTO.js is a JavaScript library that interacts with different CARTO APIs. It is part of the CARTO Engine ecosystem. + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## 4.2 - 2019-12-26 +### Added +- Improve Overview guide with npm info [2230](https://github.com/CartoDB/carto.js/issues/2230) +- New dataview filters: circle & polygon [2242](https://github.com/CartoDB/carto.js/pull/2242) + +### Fixed +- Support options dead link [2240](https://github.com/CartoDB/carto.js/issues/2240) +- Replace uglify with terser to fix internal build [2242](https://github.com/CartoDB/carto.js/pull/2242) +- Fix scroll in gradient legends [2244](https://github.com/CartoDB/carto.js/issues/2244) + +### Changed +- Update eslint [2242](https://github.com/CartoDB/carto.js/pull/2242) + + +## 4.1.11 - 2019-02-13 +### Changed +- Using Google Maps v3.35 on examples [2227](https://github.com/CartoDB/carto.js/pull/2227) + +## 4.1.10 - 2019-01-18 + +### Fixed +- Trigger view update after search [2219](https://github.com/CartoDB/carto.js/pull/2219) +- Filters: parse dates properly in the `between` filter. + +## 4.1.9 - 2019-01-09 + +### Added +- TomTom geocoder service [#2213](https://github.com/CartoDB/carto.js/issues/2213) + +### Fixed +- Histogram: fix wrong selection in last bucket. + +### Changed +- Improve examples. [#2211](https://github.com/CartoDB/carto.js/pull/2211) + +## 4.1.8 - 2018-10-29 +### Fixed +- Moved 'browserify-shim' to devDependencies, fixing a potential problem with npm shrinkwrap. + +## 4.1.7 - 2018-10-18 +### Fixed +- Client: Fix `serverUrl` parameter: `{username}` replacement, validation and documentation. + +## 4.1.6 - 2018-09-07 +### Fixed +- Source Filters: Fix return in get-object-value that was causing unintended behaviors in parameters validation. + +## 4.1.5 - 2018-09-05 +### Added +- Source Filters: allow subqueries in `eq`, `notEq` in Category filter, and `lt`, `lte`, `gt`, `gte` in Range Filter. + +## 4.1.4 - 2018-09-03 +### Added +- Source Filters: allow to filter category columns by subquery. [#2186](https://github.com/CartoDB/carto.js/issues/2186) +- Source Filters: reset filter conditions with .resetFilters(). [#2186](https://github.com/CartoDB/carto.js/issues/2186) + +### Fixed +- Source Filters: allow to combine empty filters. [#2186](https://github.com/CartoDB/carto.js/issues/2186) + +## 4.1.3 - 2018-08-09 +### Fixed +- Fix safari drag problem. [#2184](https://github.com/CartoDB/carto.js/pull/2184) + +## 4.1.2 - 2018-07-31 +### Fixed +- Fix `Promise is undefined` in IE11. [#2180](https://github.com/CartoDB/carto.js/issues/2180) + +## 4.1.1 - 2018-07-17 +### Fixed +- Fix popups/featureClick positions when scrolled. [#2179](https://github.com/CartoDB/carto.js/pull/2179) + +### Changed +- Improve examples. [#2177](https://github.com/CartoDB/carto.js/pull/2177) + +## 4.1.0 - 2018-07-13 +### Added +- Docs: Add performance tips guide. [#2168](https://github.com/CartoDB/carto.js/issues/2168) + +## 4.0.18 - 2018-07-12 +### Added +- Histogram: added `start` and `end` options to modify the histogram range. [#2142](https://github.com/CartoDB/carto.js/issues/2142) + +## 4.0.17 - 2018-07-12 +### Added +- Layers: added `visible` to options. [#2004](https://github.com/CartoDB/carto.js/issues/2004) + +### Changed +- Docs: Update 01-quickstart.md. [#2133](https://github.com/CartoDB/carto.js/pull/2133) + +### Fixed +- Fix API key in quickstart example. [#2171](https://github.com/CartoDB/carto.js/pull/2171) + +### Removed +- Remove Gemfile (compass). [#1909](https://github.com/CartoDB/carto.js/issues/1909) + +## 4.0.16 - 2018-07-10 +### Added +- Source filters: added new feature for filtering sources. [#2141](https://github.com/CartoDB/carto.js/issues/2141) + +## 4.0.15 - 2018-07-06 +### Fixed +- Allow multiple CARTO.js clients using Google Maps. [#2132](https://github.com/CartoDB/carto.js/issues/2132) + +## 4.0.14 - 2018-07-06 +### Fixed +- Dataviews: fix removeDataview not stopping fetching data [#2119](https://github.com/CartoDB/carto.js/issues/2119) + +## 4.0.13 - 2018-07-06 +### Added +- Add metrics to map instantiation. [#2139](https://github.com/CartoDB/carto.js/issues/2139) + +## 4.0.12 - 2018-07-05 +### Changed +- Debounced map instantiation. [#2140](https://github.com/CartoDB/carto.js/issues/2140) + +## 4.0.11 - 2018-07-05 +### Fixed +- Fix interactivity when only 'cartodb_id' is selected. [#2089](https://github.com/CartoDB/carto.js/issues/2089) + +## 4.0.10 - 2018-07-04 +### Added +- Dataviews: added century and millennium aggregations. [#2162](https://github.com/CartoDB/carto.js/issues/2162) +- Document new time series aggregations. [#2163](https://github.com/CartoDB/carto.js/issues/2163) + +## 4.0.9 - 2018-07-02 +### Added +- Add options as input argument to the getLeafletLayer() method. [#2125](https://github.com/CartoDB/carto.js/issues/2125) +- Add hexagon aggregation example. [#2151](https://github.com/CartoDB/carto.js/pull/2151) +- Improve structure of contents. [#2137](https://github.com/CartoDB/carto.js/pull/2137) + +### Fixed +- Docs: fix Getting Started links. [#2144](https://github.com/CartoDB/carto.js/pull/2144) +- Docs: replace 'YOUR_API_KEY' with 'default_public' in examples. [#2136](https://github.com/CartoDB/carto.js/pull/2136) +- Docs: update CDN URL in reference documentation. [#2128](https://github.com/CartoDB/carto.js/pull/2128) + +## 4.0.8 - 2018-06-04 +### Fixed +- Google Maps examples were not working on iOS. [#1995](https://github.com/CartoDB/carto.js/issues/1995) + +## 4.0.7 - 2018-06-04 +### Changed +- Update gmaps to v3.32 in v4. [#2126](https://github.com/CartoDB/carto.js/pull/2126) + +### Fixed +- Remove upper limit on Google Maps dependency. +- Small typo fixes for dev center docs. [#2124](https://github.com/CartoDB/carto.js/pull/2124) + +## 4.0.6 - 2018-05-11 +### Fixed +- Fix remove layers. [#2116](https://github.com/CartoDB/carto.js/pull/2116) + +## 4.0.5 - 2018-05-10 +- Internal fixes. + +## 4.0.4 - 2018-05-09 +- Internal fixes. + +## 4.0.3 - 2018-05-04 +### Changed +- Update zera version. [#2109](https://github.com/CartoDB/carto.js/pull/2109) + +## 4.0.2 - 2018-04-27 +### Fixed +- Add missing dependencies to release package. [#2108](https://github.com/CartoDB/carto.js/pull/2108) +- Ugrade zera to fix fractional zoom levels. [#2104](https://github.com/CartoDB/carto.js/pull/2104) + +## 4.0.1 - 2018-04-25 +### Changed +- Upgrading carto.js to gmaps v3.31. [#2067](https://github.com/CartoDB/carto.js/issues/2067) + +## 4.0.0 - 2018-04-17 +First public release of CARTO.js library +### Added +- New programmatic API +- New sources: Dataset, SQL +- New styles: CartoCSS +- New layers: Layer (Tile Layer) +- New metadata: Buckets, Categories +- New dataviews: Formula, Category, Histogram, TimeSeries +- New filters: BoundingBox, BoundingBoxLeaflet, BoundingBoxGoogleMaps +- Server aggregation options for layers +- Multiple clients support +- Return native Leaflet and Google Maps layers +- Manage layers interactivity +- Granular error management +- Publish to npm and CDN +- Public documentation within the repo +- Examples and documentation diff --git a/CHANGELOG_INTERNAL.md b/CHANGELOG_INTERNAL.md new file mode 100644 index 0000000..b217ffe --- /dev/null +++ b/CHANGELOG_INTERNAL.md @@ -0,0 +1,160 @@ +# Changelog internal + +This file contains all the changes in the **internal bundle** (used by Builder). Each change corresponds to a **pre-release version**. + +All the changes that affects the public bundle should be released as *patch* or *minor* and be included in the main Changelog. + +## 4.1.12-1 - 2019-08-06 +- Attributions widget: prevent autocollapsing and fix button bug [#2236](https://github.com/CartoDB/carto.js/pull/2236) + +## 4.1.12-0 - 2019-08-02 +- Change attribution character and toggle widget based on container [#2235](https://github.com/CartoDB/carto.js/pull/2235) + +## 4.1.11-0 - 2019-01-22 +- Enable search box geocoder provider selection [#2224](https://github.com/CartoDB/carto.js/pull/2224) + +## 4.1.10-0 - 2019-01-16 +- Change Mapbox geocoder URL to permanent one [#2221](https://github.com/CartoDB/carto.js/pull/2221) + +## 4.1.1-0 - 2018-07-13 +- Use setView instead of flyTo to improve zoom transitions. [#2178](https://github.com/CartoDB/carto.js/pull/2178) + +## 4.0.18-0 - 2018-07-12 +- Fix torque layers when analysis are applied. [#2175](https://github.com/CartoDB/carto.js/pull/2175) + +## 4.0.11-1 - 2018-07-05 +- Transpile ES6 code through Babel in Webpack 4. [#2155](https://github.com/CartoDB/carto.js/pull/2155) + +## 4.0.11-0 - 2018-07-05 +- Fix gradient legends margin. [#2166](https://github.com/CartoDB/carto.js/pull/2166) + +## 4.0.9-2 - 2018-06-29 +- Add new CARTO logo. [#2159](https://github.com/CartoDB/carto.js/pull/2159) + +## 4.0.9-1 - 2018-06-29 +- Fix choropleth legends margin. [#2157](https://github.com/CartoDB/carto.js/pull/2157) + +## 4.0.9-0 - 2018-06-19 +- Fix margins on legends. [#2143](https://github.com/CartoDB/carto.js/pull/2143) + +## 4.0.7-3 - 2018-05-24 +- Send empty string when url can not be build. [#2122](https://github.com/CartoDB/carto.js/issues/2122) + +## 4.0.7-2 - 2018-05-21 +- Avoid problems with cancelled requests in dataviews. [#2118](https://github.com/CartoDB/carto.js/pull/2118) + +## 4.0.7-1 - 2018-05-21 +- Fix legends paddings / margins. [#2121](https://github.com/CartoDB/carto.js/pull/2121) + +## 4.0.7-0 - 2018-05-17 +- Rename package to internal-carto.js. [#2120](https://github.com/CartoDB/carto.js/pull/2120) + +## 4.0.5 - 2018-05-10 +- Add new methods to cartoDB layer. [#2111](https://github.com/CartoDB/carto.js/pull/2111) + +## 4.0.4 - 2018-05-09 +- Fix layer legends margin. [#2112](https://github.com/CartoDB/carto.js/pull/2112) + +## 4.0.1-1 - 2018-04-24 +- Better popups with just images on IE and Edge. [#2105](https://github.com/CartoDB/carto.js/pull/2105) + +## 4.0.0-2 - 2018-04-24 +- Fix embed legends margin +- Enable stale bot + +## 4.0.0-beta.41 - 2018-04-17 +- Adapt to Auth API errors +- Change API menu GIF. +- Add usage box to CARTO.js examples + +## 4.0.0-beta.40 - 2018-04-12 +- Minor adjustments to legends style +- V4 docs final review +- add tip about authorization system +- Update README and CHANGELOG + +## 4.0.0-beta.39 - 2018-03-28 +- Added example for the url server parameter +- Unify engine mock + +## 4.0.0-beta.38 - 2018-03-23 +- Move template2x decision to leaflet rendering + +## 4.0.0-beta.37 - 2018-03-23 +- Propagate API keys to dataviews in public API + +## 4.0.0-beta.35 - 2018-03-19 +- Add hasBeenFetched flag to histogram + +## 4.0.0-beta.34 - 2018-03-14 +- Re-render legends when layers order is changed + +## 4.0.0-beta.33 - 2018-03-05 +- Suppress horizontal scroll in legends. Fixes IE11 + +## 4.0.0-beta.32 - 2018-02-26 +- Fix legends + +## 4.0.0-beta.31 - 2018-02-23 +- Hot size legends are broken. + +## 4.0.0-beta.29 - 2018-02-23 +- prepare shield-placement-keyword CartoCSS property + +## 4.0.0-beta.28 - 2018-02-21 +- 2046 move docs + +## 4.0.0-beta.27 - 2018-02-21 +- Adjust styles mobile view embed maps [WIP] + +## 4.0.0-beta.26 - 2018-02-20 +- add marker size to layer cartocss props to reinstantiate torque map + +## 4.0.0-beta.24 - 2018-02-15 +- Freeze GMaps script version +- Warn instead of error if no API key +- Fix popup examples for polygons +- Revert "Hide aggregation from public api" (#2029) +- Added server aggregation validation #2032 +- Check parameters when creating bounding box filters. #2003 + +## 4.0.0-beta.23 - 2018-02-06 +- Update all examples to Leaflet1.3.1 +- remove unnecessary`sync_on_data_change` #2036 + +## 4.0.0-beta.22 - 2018-02-01 +- Use retina url in high resolution screens + +## 4.0.0-beta.21 - 2018-02-01 +- Set Leaflet zoomAnimationThreshold to 1000 (internal only) +- Define layers order + +## 4.0.0-beta.20 - 2018-02-01 +- Remove tangram support + +## 4.0.0-beta.19 - 2018-02-01 +- Examples feedback +- 2011 autogenerate changelog + +## 4.0.0-beta.18 - 2018-01-31 +- Override scrollwheel if it's boolean + +## 4.0.0-beta.17 - 2018-01-30 +- 5067 add mapbox geocoder + +## 4.0.0-beta.16 - 2018-01-30 +- Append legends view if js-embed-legends is found + +## 4.0.0-beta.15 - 2018-01-29 +- Remove category value stringify + +## 4.0.0-beta.14 - 2018-01-19 +- Merge pull request #2017 from CartoDB/11341-add-dblclick-event +- 11341 add dblclick event + +## 4.0.0-beta.13 - 2018-01-18 +- add layer zoom options (#2002) +- Add aggregation options to layer constructor (#2010) + +## 4.0.0-beta.12 - 2018-01-18 +- Allow custom IDs in layers (#2000) diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..4f06b43 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,155 @@ +var _ = require('underscore'); +var jasmineCfg = require('./grunt/tasks/jasmine'); + +function getTargetDiff () { + // Detect changed files. If no changes return '.' (all files) + var target = require('child_process').execSync('(git diff --name-only --relative || true;)' + + '| grep \'\\.js\\?$\' || true').toString(); + if (target.length === 0) { + target = ['.']; + } else { + target = target.split('\n'); + target.splice(-1, 1); + } + return target; +} + +/** + * Grunfile runner file for CARTO.js + * framework + * + */ +module.exports = function (grunt) { + require('load-grunt-tasks')(grunt); + require('time-grunt')(grunt); + var semver = require('semver'); + + var version = grunt.file.readJSON('package.json').version; + + if (!version || !semver.valid(version)) { + grunt.fail.fatal('package.json version is not valid', 1); + } + + grunt.initConfig({ + secrets: {}, + dist: 'dist', + tmp: '.tmp', + version: version, + gitinfo: {}, + browserify: require('./grunt/tasks/browserify').task(), + exorcise: require('./grunt/tasks/exorcise').task(), + s3: require('./grunt/tasks/s3').task(version), + fastly: require('./grunt/tasks/fastly').task(), + sass: require('./grunt/tasks/scss').task(), + watch: require('./grunt/tasks/watch').task(), + connect: require('./grunt/tasks/connect').task(), + copy: require('./grunt/tasks/copy').task(), + clean: require('./grunt/tasks/clean').task(), + concat: require('./grunt/tasks/concat').task(), + terser: require('./grunt/tasks/terser').task(), + usebanner: require('./grunt/tasks/usebanner').task(), + cssmin: require('./grunt/tasks/cssmin').task(), + imagemin: require('./grunt/tasks/imagemin').task(), + jasmine: jasmineCfg, + eslint: { target: getTargetDiff() } + }); + + grunt.registerTask('publish_s3', function (target) { + if (!grunt.file.exists('secrets.json')) { + grunt.fail.fatal('secrets.json file does not exist, copy secrets.example.json and rename it', 1); + } + + // Read secrets + grunt.config.set('secrets', grunt.file.readJSON('secrets.json')); + + if (!grunt.config('secrets') || + !grunt.config('secrets').AWS_USER_S3_KEY || + !grunt.config('secrets').AWS_USER_S3_SECRET || + !grunt.config('secrets').AWS_S3_BUCKET + ) { + grunt.fail.fatal('S3 keys not specified in secrets.json', 1); + } + + grunt.task.run([ + 's3' + ]); + }); + + grunt.registerTask('invalidate', function () { + if (!grunt.file.exists('secrets.json')) { + grunt.fail.fatal('secrets.json file does not exist, copy secrets.example.json and rename it', 1); + } + + // Read secrets + grunt.config.set('secrets', grunt.file.readJSON('secrets.json')); + + if (!grunt.config('secrets') || + !grunt.config('secrets').FASTLY_API_KEY || + !grunt.config('secrets').FASTLY_CARTODB_SERVICE + ) { + grunt.fail.fatal('Fastly keys not specified in secrets.json', 1); + } + + grunt.task.run([ + 'fastly' + ]); + }); + + grunt.registerTask('preWatch', function () { + grunt.config('doWatchify', true); // required for browserify to use watch files instead + }); + + grunt.registerTask('build-jasmine-specrunners', _ + .chain(jasmineCfg) + .keys() + .map(function (name) { + return ['jasmine', name, 'build'].join(':'); + }) + .value()); + + // Define tasks order for each step as if run in isolation, + // when registering the actual tasks _.uniq is used to discard duplicate tasks from begin run + var allDeps = [ + 'clean:dist_internal', + 'gitinfo', + 'copy:fonts' + ]; + var css = allDeps + .concat([ + 'sass', + 'concat', + 'cssmin', + 'imagemin' + ]); + var js = allDeps + .concat([ + 'browserify', + 'build-jasmine-specrunners' + ]); + var buildJS = allDeps + .concat(js) + .concat([ + 'exorcise', + 'terser', + 'usebanner' + ]); + var devJS = allDeps + .concat('preWatch') + .concat(js); + var watch = [ + 'connect', + 'watch' + ]; + + grunt.registerTask('default', [ 'build' ]); + grunt.registerTask('build', _.uniq(buildJS.concat(css))); + grunt.registerTask('build:js', _.uniq(buildJS)); + grunt.registerTask('build:css', _.uniq(css)); + grunt.registerTask('test', _.uniq(js.concat([ + 'eslint', + 'jasmine' + ]))); + grunt.registerTask('dev', _.uniq(css.concat(devJS).concat(watch))); + grunt.registerTask('dev:css', _.uniq(css.concat(watch))); + grunt.registerTask('dev:js', _.uniq(devJS.concat(watch))); +}; diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ce08618 --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2018, CARTO +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3d54596 --- /dev/null +++ b/README.md @@ -0,0 +1,105 @@ +# CARTO.js 4 + +CARTO.js is a JavaScript library to create custom location intelligence applications that leverage the power of [CARTO](https://carto.com/). It is the library that powers [Builder](https://carto.com/builder/) and it is part of the [Engine](https://carto.com/pricing/engine/) ecosystem. + +## Getting Started + +The best way to get started is to navigate through the CARTO.js documentation site: + +- [Guide](https://carto.com/developers/carto-js/guides/quickstart/) will give you a good overview of the library. +- [API Reference](https://carto.com/developers/carto-js/reference/) will help you use a particular class or method. +- [Examples](https://carto.com/developers/carto-js/examples/) will demo some specific features. +- [Support](https://carto.com/developers/carto-js/support/) might answer some of your questions. + +## Versioning + +We use [SemVer](http://semver.org/) for versioning. + +Please refer to [CHANGELOG.md](CHANGELOG.md) for a list of notables changes for each version of the library. + +You can also see the [tags on this repository](https://github.com/CartoDB/carto.js/tags). + +## Submitting Contributions + +You will need to sign a Contributor License Agreement (CLA) before making a submission. [Learn more here.](https://carto.com/contributions/) + +## License + +This project is licensed under the BSD 3-clause "New" or "Revised" License - see the [LICENSE.txt](LICENSE.txt) file for details. + +## Documentation + +### API Reference + +Run `npm run docs` to build the API reference documentation from jsdoc annotations. + +Once the task is done, you can visit `docs/public/index.html` to check the reference + +### General documentation + +You can read the general documentation that is published at [https://carto.com/developers/carto-js/](https://carto.com/developers/carto-js/) also in this repo. They are written in Markdown. + +Warning: internal links in these documents don't work. They are replaced when the documentation is published in [https://carto.com/](https://carto.com/developers/carto-js/) + + +#### Guides + +The folder `docs/guides` contains general information about the CARTO.js library. + +- Quickstart: get started quickly following this tutorial. +- Upgrade considerations: if you have experience with previous versions of CARTO.js, this is the place to learn the differences between the former library and the newest one. +- Glossary: terms that appear throughout the documentation. + +#### Examples + +In the folder `examples/public` you can find several folders with example for every feature of CARTO.js. + +#### Reference topics + +The document `docs/reference/topics.md` contains general considerations when working with CARTO.js. It's advisable to read them before diving in the API reference. + +#### Support + +The folder `docs/support` contains several document with support documentation: support options, FAQs, error messages... + +## Development + +### Run the tests + +``` +npm test +``` + +### Build the library + +``` +npm run build +``` + +To watch the files + +``` +npm run build:watch +``` + +### Generate the docs + +``` +npm run docs +``` + +### Release version + +``` +npm run bump +``` + +To publish a release to the `CDN` and `npm` + +``` +npm run release +``` + + +## Looking for the previous version? +Previous version cartodb.js v3 it's available [here](https://github.com/CartoDB/carto.js/tree/develop) diff --git a/SECURITY-POLICY.md b/SECURITY-POLICY.md new file mode 100644 index 0000000..5c45bbe --- /dev/null +++ b/SECURITY-POLICY.md @@ -0,0 +1,3 @@ +Please read [security.txt](https://github.com/CartoDB/carto.js/blob/master/security.txt) to report any security vulnerabilities. We will acknowledge receipt of your vulnerability report and strive to send you regular updates about our progress. If you're curious about the status of your disclosure please feel free to email us again. + +Please refrain from requesting compensation for reporting vulnerabilities. If you want we will publicly acknowledge your responsible disclosure, once the issue is fixed. diff --git a/config/jsdoc/index.html b/config/jsdoc/index.html new file mode 100644 index 0000000..f473411 --- /dev/null +++ b/config/jsdoc/index.html @@ -0,0 +1,18 @@ + + + + + CARTO.js + + + +

CARTO.js v4

+ +

Version: %VERSION

+ + diff --git a/config/jsdoc/internal-conf.json b/config/jsdoc/internal-conf.json new file mode 100644 index 0000000..2529e7c --- /dev/null +++ b/config/jsdoc/internal-conf.json @@ -0,0 +1,20 @@ +{ + "source": { + "include": [ + "src" + ] + }, + "templates": { + "default": { + "outputSourceFiles": false, + "useLongnameInNav": true + } + }, + "plugins": [ + "plugins/markdown" + ], + "opts": { + "recurse": true, + "destination": "./docs/internal" + } +} diff --git a/config/jsdoc/plugins/api.js b/config/jsdoc/plugins/api.js new file mode 100644 index 0000000..c086478 --- /dev/null +++ b/config/jsdoc/plugins/api.js @@ -0,0 +1,26 @@ +/** + * Define @api tag + */ +exports.defineTags = function (dictionary) { + dictionary.defineTag('api', { + mustHaveValue: false, + canHaveType: false, + canHaveName: false, + onTagged: function (doclet, tag) { + doclet.public = true; + } + }); +}; + +/* + * Only items with @api annotation should be documented + */ + +exports.handlers = { + parseComplete: function (e) { + var doclets = e.doclets; + for (var i = 0; i < doclets.length; i++) { + doclets[i].undocumented = !doclets[i].public; + } + } +}; diff --git a/config/jsdoc/public-conf.json b/config/jsdoc/public-conf.json new file mode 100644 index 0000000..73989f2 --- /dev/null +++ b/config/jsdoc/public-conf.json @@ -0,0 +1,21 @@ +{ + "source": { + "include": [ + "src" + ] + }, + "templates": { + "default": { + "outputSourceFiles": false, + "useLongnameInNav": true + } + }, + "plugins": [ + "plugins/api", + "plugins/markdown" + ], + "opts": { + "recurse": true, + "destination": "./docs/public" + } +} diff --git a/docs/guides/01-overview.md b/docs/guides/01-overview.md new file mode 100644 index 0000000..2830ff9 --- /dev/null +++ b/docs/guides/01-overview.md @@ -0,0 +1,172 @@ +## Overview + +CARTO.js lets you create custom location intelligence applications that leverage the power of the CARTO Platform. + +### Audience + +This documentation is designed for people familiar with JavaScript programming and object-oriented programming concepts. You should also be familiar with [Leaflet](https://leafletjs.com/) from a developer's point of view. + +This conceptual documentation is designed to let you quickly start exploring and developing applications with the CARTO.js library. We also publish the [CARTO.js API Reference]({{site.cartojs_docs}}/reference/). + +### Hello, World + +The easiest way to start learning about the CARTO.js library is to see a simple example. The following web page displays a map adding a layer over it. + +```html + + + + Single layer | CARTO + + + + + + + + + + + + +
+
+ + + + + + +``` + +[View example]({{site.cartojs_docs}}/examples/#example-add-a-layer). + +Even in this simple example, there are a few things to note: + + - We declare the application as HTML5 using the `` declaration. + - We load the CARTO.js library using a `script` tag. + - We create a `div` element named "map" to hold the map. + - We define the JavaScript that creates a map in the `div`. + +These steps are explained below. + +### Declaring your application as HTML5 + +We recommend that you declare a true DOCTYPE within your web application. Within the examples here, we've declared our applications as HTML5 using the simple HTML5 DOCTYPE as shown below: + +```html + +``` + +Most current browsers will render content that is declared with this DOCTYPE in "standards mode" which means that your application should be more cross-browser compliant. The DOCTYPE is also designed to degrade gracefully; browsers that don't understand it will ignore it, and use "quirks mode" to display their content. + +We add styles to the map through the file `style.css`, declaring: + +```css +body { + margin: 0; + padding: 0; +} + +#map { + position: absolute; + height: 100%; + width: 100%; + z-index: 0; +} +``` + +This CSS declaration indicates that the map container
(with id map) should take up 100% of the height of the HTML body. + +### Loading the CARTO.js library + +To load the Maps JavaScript API, use a script tag like the one in the following example: + +```html + +``` + +The URL contained in the script tag is the location of a JavaScript file that loads all of the code you need for using the CARTO.js library. This script tag is required. We are using the minified version of the library. + +**Tip:** If you have experience with **npm** and a build system in your project (webpack, rollup…), you can install CARTO.js library with `npm install @carto/carto.js`. Then you can import it easily with `import carto from '@carto/carto.js` (or `var carto = require('@carto/carto.js')`, depending on your module system). + + +#### HTTPS or HTTP +We think security on the web is pretty important, and recommend using HTTPS whenever possible. As part of our efforts to make the web more secure, we've made all of the CARTO components available over HTTPS. Using HTTPS encryption makes your site more secure, and more resistant to snooping or tampering. + +We recommend loading the CARTO.js library over HTTPS using the ` + + + + + + + + +
+
+
+
+

European countries

+ +
+
+

Average population

+

xxx inhabitants

+
+
+
+ + + +``` + +The first part of the skeleton loads CARTO.js and Leaflet 1.2.0 assumes Leaflet is loaded in `window.L`. The library checks if the version of Leaflet is compatible. If not, it throws an error. + +The app skeleton also loads CARTO.js, adds some necessary elements, and defines a `script` tag. This is where you will write all the JavaScript code to make the example work. + +**Note:** While CARTO.js enables you to display data from CARTO on top of Leaflet or Google Maps map, the actual map settings are controlled via the [Leaflet](http://leafletjs.com/) or [Google Maps](https://hpneo.github.io/gmaps/) map options. + +#### Creating the Leaflet Map + +CARTO.js apps start from a Leaflet or Google Map. Let's use a basic [Leaflet](http://leafletjs.com/) map for this guide: + +```javascript +const map = L.map('map').setView([50, 15], 4); +``` + +#### Adding Basemap and Label Layers + +Define the type of basemap to be used for the background of your map. For this guide, let's use [CARTO's Voyager basemap](https://carto.com/location-data-services/basemaps/). + +```javascript +// Adding Voyager Basemap +L.tileLayer('https://{s}.basemaps.cartocdn.com/rastertiles/voyager_nolabels/{z}/{x}/{y}.png', { + maxZoom: 18 +}).addTo(map); + +// Adding Voyager Labels +L.tileLayer('https://{s}.basemaps.cartocdn.com/rastertiles/voyager_only_labels/{z}/{x}/{y}.png', { + maxZoom: 18, + zIndex: 10 +}).addTo(map); +``` + +`L.tileLayer` creates a layer for the basemap and the `zIndex` property defines the basemap labels (that sit on top of the other layers). + +### Defining a `carto.Client` + +`carto.Client` is the entry point to CARTO.js. It handles the communication between your app and your CARTO account, which is defined by your API Key and your username. + +```javascript +var client = new carto.Client({ + apiKey: '{API Key}', + username: '{username}' +}); +``` + +**Warning:** Ensure that you modify any placeholder parameters shown in curly brackets with your own credentials. For example, `apiKey: '123abc',` and `username: 'john123'`. + +### Displaying Data on the Map + +Display data hosted on your CARTO account as map layers. + +### Defining Layers + +Layers are defined with `carto.layer.Layer` which include the dataset name and basic styling options with CartoCSS. + +```javascript +const europeanCountriesDataset = new carto.source.Dataset(` + ne_adm0_europe +`); +const europeanCountriesStyle = new carto.style.CartoCSS(` + #layer { + polygon-fill: #162945; + polygon-opacity: 0.5; + ::outline { + line-width: 1; + line-color: #FFFFFF; + line-opacity: 0.5; + } + } +`); +const europeanCountries = new carto.layer.Layer(europeanCountriesDataset, europeanCountriesStyle); +``` +The first layer defines a `carto.source.Dataset` object that points to the imported dataset named `ne_adm0_europe`. To style the layer, define a `carto.style.CartoCSS` object that describes how polygons will be rendered. + +```javascript +const populatedPlacesSource = new carto.source.SQL(` + SELECT * + FROM ne_10m_populated_places_simple + WHERE adm0name IN (SELECT admin FROM ne_adm0_europe) +`); +const populatedPlacesStyle = new carto.style.CartoCSS(` + #layer { + marker-width: 8; + marker-fill: #FF583E; + marker-fill-opacity: 0.9; + marker-line-width: 0.5; + marker-line-color: #FFFFFF; + marker-line-opacity: 1; + marker-type: ellipse; + marker-allow-overlap: false; + } +`); +const populatedPlaces = new carto.layer.Layer(populatedPlacesSource, populatedPlacesStyle, { + featureOverColumns: ['name'] +}); +``` + +The second layer uses a more complex type of data source for the layer. `carto.source.SQL` provides more flexibility for defining the data that you want to display in a layer. For this guide, we are composing a `SELECT` statement that selects populated cities in Europe. (Optionally, you can change this query at runtime). + +The `featureOverColumns: ['name']` option we include in the layer creation defines what columns of the datasource will be available in `featureOver` events as we explain below. + +#### Adding Layers to the Client + +Before you can add layers to the map, the `carto.Client` variable needs to be notified that these layers exist. The client is responsible for grouping layers into a single Leaflet layer. + +```javascript +client.addLayers([europeanCountries, populatedPlaces]); +``` + +#### Adding Layers to the Map + +Now that the client recognizes these two layers, this single Leaflet layer can be added to the map. + +```javascript +client.getLeafletLayer().addTo(map); +``` + +As a result, your map should display polygons for each European country and red points represent populated cities. + +**Tip:** In order to change the order of layers, flip the order from `([A, B]);` to `(B, A);` + +### Setting up Tooltips + +Tooltips give map viewers information about the underlying data as they interact with the map by clicking or hovering over data. CARTO.js provides a simple mechanism to detect some of these interactions and use the data associated to them. For this guide, let's display a mouse hover tooltip showing the name of a city. + +#### Showing the Tooltip when User Mouses Over a City + +Layers trigger `featureOver` events when the map viewer hovers the mouse over a feature. The event includes data about the feature, such as the latitude and longitude, as well as specified column values defined in `featureOverColumns`. + +```javascript +const popup = L.popup({ closeButton: false }); +populatedPlaces.on(carto.layer.events.FEATURE_OVER, featureEvent => { + popup.setLatLng(featureEvent.latLng); + if (!popup.isOpen()) { + popup.setContent(featureEvent.data.name); + popup.openOn(map); + } +}); +``` + +In this snippet, we are defining a `L.popup` and listening to the `featureOver` event. When the event is triggered, the pop-up is positioned, populated with the name of the city, and added to the map. + + +#### Hiding the Tooltip + +Similarly, layers trigger `featureOut` events when the map viewer is no longer moving the mouse over a feature. Use this event to hide the pop-up. + +```javascript +populatedPlaces.on(carto.layer.events.FEATURE_OUT, featureEvent => { + popup.removeFrom(map); +}); +``` + +When `featureOut` is triggered, this code removes the pop-up from the map. + +### Creating a Country Selector Widget + +This section describes how to get data from a previously defined data source and display a country selector the map. As a result, selecting a country on the map highlights the country and filters by populated places. + +#### Defining a Category Dataview + +Dataviews are the mechanism CARTO.js uses to access data from a data source (dataset or SQL query) in a particular way (eg: list of categories, result of a formula, etc.). Use the `carto.dataview.Category` dataview to get the names of the European countries in the dataset: + +```javascript +const countriesDataview = new carto.dataview.Category(europeanCountriesDataset, 'admin', { + limit: 100 +}); +``` + +This type of dataview expects a data source and the name of the column with the categories. By default, results are limited. For this guide, we specified a limit of 100 categories to make sure all country names are returned. + +#### Listening to Data Changes on the Dataview + +In order to know when a dataview has new data (eg: right after it has been added to the client or when its source has changed), you should listen to the `dataChanged` event. This event gives you an object with all the categories. + +```javascript +countriesDataview.on('dataChanged', data => { + const countryNames = data.categories.map(category => category.name).sort(); + refreshCountriesWidget(countryNames); +}); + +function refreshCountriesWidget(adminNames) { + const widgetDom = document.querySelector('#countriesWidget'); + const countriesDom = widgetDom.querySelector('.js-countries'); + + countriesDom.onchange = event => { + const admin = event.target.value; + highlightCountry(admin); + filterPopulatedPlacesByCountry(admin); + }; + + // Fill in the list of countries + adminNames.forEach(admin => { + const option = document.createElement('option'); + option.innerHTML = admin; + option.value = admin; + countriesDom.appendChild(option); + }); +} + +function highlightCountry(admin) { + let cartoCSS = ` + #layer { + polygon-fill: #162945; + polygon-opacity: 0.5; + ::outline { + line-width: 1; + line-color: #FFFFFF; + line-opacity: 0.5; + } + } + `; + if (admin) { + cartoCSS = ` + ${cartoCSS} + #layer[admin!='${admin}'] { + polygon-fill: #CDCDCD; + } + `; + } + europeanCountriesStyle.setContent(cartoCSS); +} + +function filterPopulatedPlacesByCountry(admin) { + let query = ` + SELECT * + FROM ne_10m_populated_places_simple + WHERE adm0name IN (SELECT admin FROM ne_adm0_europe) + `; + if (admin) { + query = ` + SELECT * + FROM ne_10m_populated_places_simple + WHERE adm0name='${admin}' + `; + } + populatedPlacesSource.setQuery(query); +} +``` + +This snippet generates the list of country names in alphabetical order from the `dataChanged` parameter and uses the `SELECT` function to populate the list. + +When a country is selected, two functions are invoked: `highlightCountry` (highlights the selected country by setting a new CartoCSS to the layer) and `filterPopulatedPlacesByCountry` (filters the cities by changing the SQL query). + +#### Adding the Dataview to the Client + +The dataview needs to be added to the client in order to fetch data from CARTO. + +```javascript +client.addDataview(countriesDataview); +``` + +### Creating a Formula Widget + +Now that you have a working country selector, let's add a widget that will display the average max population of the populated places for the selected country (or display ALL if no country is selected). + +#### Defining a Formula Dataview + +`carto.dataview.Formula` allows you to execute aggregate functions (count, sum, average, max, min) on a data source: + +```javascript +const averagePopulation = new carto.dataview.Formula(populatedPlacesSource, 'pop_max', { + operation: carto.operation.AVG +}); +``` + +The `averagePopulation` dataview triggers a `dataChanged` event every time a new average has been calculated. + +#### Listening to Data Changes on the Dataview + +The `dataChanged` event allows you to get results of the aggregate function and use it in many ways. + +```javascript +averagePopulation.on('dataChanged', data => { + refreshAveragePopulationWidget(data.result); +}); + +function refreshAveragePopulationWidget(avgPopulation) { + const widgetDom = document.querySelector('#avgPopulationWidget'); + const averagePopulationDom = widgetDom.querySelector('.js-average-population'); + averagePopulationDom.innerText = Math.floor(avgPopulation); +} +``` + +This snippet refreshes the averaged population widget every time a new average is available (e.g., when a new country is selected). + +#### Adding the dataview to the client + +To get a full working dataview, add it to your client: + +```javascript +client.addDataview(averagePopulation); +``` + +### Conclusion + + + +### Troubleshooting and support + +For more information on using the CARTO.js library, take a look at the [support page]({{site.cartojs_docs}}/support/). + +The CARTO.js library may issue an error or warning when something goes wrong. You should check for warnings in particular if you notice that something is missing. It's also a good idea to check for warnings before launching a new application. Note that the warnings may not be immediately apparent because they appear in the HTTP header. For more information, see the guide to [errors messages]({{site.cartojs_docs}}/support/error-messages/). + +This guide is just an overview of how to use CARTO.js to overlay data and create widgets. View the [Examples]({{ site.cartojs_docs }}/examples/) section for specific features of CARTO.js in action. diff --git a/docs/guides/04-performance-tips.md b/docs/guides/04-performance-tips.md new file mode 100644 index 0000000..842c188 --- /dev/null +++ b/docs/guides/04-performance-tips.md @@ -0,0 +1,101 @@ +## Performance Tips + +As you go through developing your applications with CARTO.js, you might find useful some tips to ease the development and make your application more performant. + +### Sources +Sources are the objects to get data from. The source is the entity that points to the data we want to show in our layers or in a widget through a dataview. + +When working with sources, you may want to change the actual query to show other data in your visualization or dataviews. The most common case is filtering your data. The layers and dataviews react to changes coming from linked sources, so that you don't have to create another source. Layers and dataviews that are linked to the source reflect the changes in the source. + +The way to go is to call `.setQuery` method to update the SQL query when using a `carto.source.SQL`, or use `.setTableName` in a `carto.source.Dataset` source, like in this examples: + +``` js +const populationSource = new carto.source.SQL('SELECT * FROM your_dataset'); +populationSource.setQuery('SELECT * FROM your_dataset WHERE price < 80'); +``` + +![setQuery diagram](../../img/set_query_diagram.svg) + +``` js +const populationDataset = new carto.source.Dataset('your_dataset'); +populationDataset.setTableName('another_dataset'); +``` + +![setTableName diagram](../../img/set_table_name_diagram.svg) + +That way, the dataviews and visualizations retrieving data from that source will be automatically updated without doing anything else on your part. + +### Styles +We need to use CartoCSS whenever we want to change the style of our markers or polygons, among other things. Each CartoCSS instance contains the styles we want to apply to any of our layers. + +These style instances work the same way as sources do. It is pretty common to change styles in your map based on certain triggers, so that you can adequate your visualization to what you want to show. + +When linked to a layer, it will automatically show the style change when invoking `.setContent` on the style object with a string containing the new style content. + +```js +const layerStyle = new carto.style.CartoCSS(` + #layer { + marker-width: 8; + marker-fill: #FF583E; + marker-fill-opacity: 0.9; + marker-allow-overlap: false; + } +`); + +layerStyle.setContent(` + #layer { + marker-width: 10; + marker-fill: #FF583E; + marker-allow-overlap: true; + } +`); +``` + +![setContent diagram](../../img/set_content_diagram.svg) + +### Layers +Layers are a fundamental part of your CARTO.js application. They show the data of a source using the style of a CartoCSS. + +As stated before, the layer will be automatically updated in the map when any of its properties (source and style) change. + +So, let's say that you want to update the table of your visualization which had been created like this: + +``` js +const populationSource = new carto.source.SQL('SELECT * FROM your_dataset'); +const layerStyle = new carto.style.CartoCSS(` + #layer { + marker-width: 10; + marker-fill: #FF583E; + marker-allow-overlap: true; + } +`); + +const layer = new carto.layer.Layer(populationSource, layerStyle); +``` + +To update the visualization, the only thing you need to do is to invoke `.setQuery` in your source and everything will be refreshed accordingly. + +```js +populationSource.setQuery('SELECT * FROM your_dataset WHERE price > 500'); +``` + +### Dataviews +Dataviews are a way to extract data from our source in predefined ways depending on the type of the column (eg: a list of categories, the result of a formula operation, etc...). + +Dataviews need a source to extract data from. So when a source is passed to a dataview, it will react to the changes happening to the source, whether changing the query or adding filters. + +```js +const populatedPlaces = new carto.source.Dataset('ne_10m_populated_places_simple'); + +const column = 'adm0name'; // Aggregate the data by country. +const categoryDataview = new carto.dataview.Category(populatedPlaces, column, { + operation: carto.operation.AVG, // Compute the average + operationColumn: 'pop_max' // The name of the column where the operation will be applied. +}); + +// ... + +citiesSource.addFilter(new carto.filter.Category(column, { eq: 'Spain' } )); +``` + +In the example above, first we set a dataview getting data from a dataset with information about populated places. Then, we add a filter to the source to show only data coming from Spain. As you can see, we've updated the source and since the dataview was created linked to that source, a change on the query makes the dataview to react to that change automatically. There's no need to create another source and then another dataview. diff --git a/docs/guides/05-upgrade-considerations.md b/docs/guides/05-upgrade-considerations.md new file mode 100644 index 0000000..7686b7c --- /dev/null +++ b/docs/guides/05-upgrade-considerations.md @@ -0,0 +1,181 @@ +## Upgrade Considerations Guide + +This document is intended for existing developers who have used [previous versions]({{site.cartojs_docs}}/reference/#versioning) of CARTO.js. + +### About this Guide + +This guide describes how the CARTO.js library has changed to support additional functionality. It outlines the basic workflow for creating an application and includes an example of updating an old application using the new library. + +**Tip**: The authorization system behaves in a uniform way for any version of CARTO.js. You can read about the [fundamentals of authorization]({{site.fundamental_docs}}/authorization/) or know implementation details of the [Auth API]({{site.authapi_docs}}/) under the hood. + +At a high-level, the workflow consists of: + + 1. Define the client parameters to manage layers and dataviews: + - [`new carto.Client`]({{site.cartojs_docs}}/reference/#cartoclient) + - [`addLayers`]({{site.cartojs_docs}}/reference/#cartoclientaddlayer) + - [`addDataview`]({{site.cartojs_docs}}/reference/#cartoclientadddataview) or [`addDataviews`]({{site.cartojs_docs}}/reference/#cartoclientadddataviews) + - [`getDataviews`]({{site.cartojs_docs}}/reference/#cartoclientgetdataviews) + +2. Define the Base data source objects: + - Add dataset as the source [`carto.source.dataset`]({{site.cartojs_docs}}/reference/#cartosourcedataset) + - Add SQL to filter data [`carto.source.sql`]({{site.cartojs_docs}}/reference/#cartosourcesql) + +3. Publish the App: + - [`getLayers`]({{site.cartojs_docs}}/reference/#cartoclientgetlayers) + +You should understand the following changes in concept before you begin. + +#### Dataset Privacy + +Since we now have a new authorization system for the entire CARTO platform, directly related to dataset privacy, datasets can be public and private as well. Read the [basics of authorization]({{site.fundamental_docs}}/authorization/) to learn more about this aspect of the CARTO platform. + +#### Map Workflow + +`new carto.Client` is the main entry point for building your application. This enables you to communicate between your app and your CARTO account by using your API Key. This enhancement clearly identifies client requests separate from visualization requests. + +#### `createVis` + +The current beta of CARTO.js only includes a JavaScript library, it does not include `creatVis` components to maintain your app. A future enhancement of the library will include functionality for maintaining your core application. + +#### `Dataview` + +A Dataview enables you to create different views of data stored in a table. CARTO.js uses `Dataviews` to add interactive widgets for viewing and filtering map data. + +#### SQL API Integration + +CARTO.js no longer includes a client for the SQL API. Developers looking to get data from their CARTO account can query SQL API with AJAX or the new [JS Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API).. + +## Upgrading an Existing Application + +Suppose you have an app that was created with an earlier version of CARTO.js? This guide provides an example of CARTO.js components showing the old code modified with updated code. + +The following example shows the application skeleton using version 3.15 of CARTO.js. + +```html + + + + + + + + + + + + + + +
+ + + + +``` + +In order to make this example work with version 4 of CARTO.js, modify the code as follows: + +```html + + + + + + + + + + + + + + + +
+ + + + +``` + +#### Conclusion + +For more details about how to use CARTO.js, [view the examples]({{site.cartojs_docs}}/examples/) section for specific features of CARTO.js in action. diff --git a/docs/guides/06-glossary.md b/docs/guides/06-glossary.md new file mode 100644 index 0000000..b89c65e --- /dev/null +++ b/docs/guides/06-glossary.md @@ -0,0 +1,35 @@ +## Glossary + +This glossary defines terms that appear throughout the CARTO.js documentation. + +### A + +#### Ajax + +Asynchronous JavaScript + XML, while not a technology in itself, is a term coined in 2005 by Jesse James Garrett, that describes a "new" approach to using a number of existing technologies together, including HTML or XHTML, Cascading Style Sheets, JavaScript, The Document Object Model, XML, XSLT, and most importantly the XMLHttpRequest object. More info about Ajax at [Ajax](https://developer.mozilla.org/en-US/docs/Web/Guide/AJAX). + +### C + +#### Client + +Throughout CARTO.js documentation, `client` refers to the object used as an entry point to CARTO.js features. + +It's the object used to add your account credentials and to manage layers and dataviews. + +More info at [carto.Client]({{site.cartojs_docs}}/reference/#cartoclient) + +### L + +#### Layer + +A layer object is used to visualize geospatial data in CARTO.js. They have a source, where the data comes from, and a style, defining how you want the layer to look like. + +They are showed on top of a Leaflet or a Google map. + +[Layer reference]({{site.cartojs_docs}}/reference/#cartolayerlayer) + +### P + +#### Promise + +Promise objects are a standard way for handling asynchronous tasks in Javascript. More info about promises at [Using Promises - MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises). diff --git a/docs/guides/quickstart-example.html b/docs/guides/quickstart-example.html new file mode 100644 index 0000000..37ba715 --- /dev/null +++ b/docs/guides/quickstart-example.html @@ -0,0 +1,203 @@ + + + + Guide | CARTO + + + + + + + + + + + + +
+
+
+
+

European countries

+ +
+
+

Average population

+

xxx inhabitants

+
+
+
+ + + diff --git a/docs/img/avatar.gif b/docs/img/avatar.gif new file mode 100644 index 0000000000000000000000000000000000000000..bf1a509556baf9364f11bdaf9029a26c34f104ce GIT binary patch literal 191484 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%0AWIlHpZkH3?*Ty0 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

ZzE9e>x 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 literal 0 HcmV?d00001 diff --git a/docs/img/set_content_diagram.svg b/docs/img/set_content_diagram.svg new file mode 100644 index 0000000..b796956 --- /dev/null +++ b/docs/img/set_content_diagram.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/docs/img/set_query_diagram.svg b/docs/img/set_query_diagram.svg new file mode 100644 index 0000000..d965fe7 --- /dev/null +++ b/docs/img/set_query_diagram.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/img/set_table_name_diagram.svg b/docs/img/set_table_name_diagram.svg new file mode 100644 index 0000000..d71372c --- /dev/null +++ b/docs/img/set_table_name_diagram.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/reference/01-introduction.md b/docs/reference/01-introduction.md new file mode 100644 index 0000000..411cba8 --- /dev/null +++ b/docs/reference/01-introduction.md @@ -0,0 +1,9 @@ +## Introduction + +CARTO.js is a JavaScript library that interacts with different CARTO APIs. It is part of the [CARTO Engine](https://carto.com/pricing/engine/) ecosystem. + +To understand the fundamentals of CARTO.js v4, [read the guides]({{site.cartojs_docs}}/guides/quickstart/). To view the source code, browse the [open-source repository](https://github.com/CartoDB/carto.js) in Github and contribute. Otherwise, view [examples with Leaflet and Google Maps]({{site.cartojs_docs}}/examples/) or find different [support options]({site.cartojs_docs}}/support/support-options/). + +If you find any trouble understanding any term written in this reference, please visit our [glossary]({{site.cartojs_docs}}/guides/glossary/) + +The contents described in this document are subject to CARTO's [Terms of Service](https://carto.com/legal/) diff --git a/docs/reference/02-authentication.md b/docs/reference/02-authentication.md new file mode 100644 index 0000000..1b7e228 --- /dev/null +++ b/docs/reference/02-authentication.md @@ -0,0 +1,16 @@ +## Authentication + +CARTO.js v4 requires using an API Key. From your CARTO dashboard, click _[Your API keys](https://carto.com/login)_ from the avatar drop-down menu to view your uniquely generated API Key for managing data with CARTO Engine. + +![Your API Keys](../img/avatar.gif) + +Learn more about the [basics of authorization]({{site.fundamental_docs}}/authorization/), or dig into the details of [Auth API]({{site.authapi_docs}}/), if you want to know more about this part of CARTO platform. + +The examples in this documentation include a placeholder for the API Key. Ensure that you modify any placeholder parameters with your own credentials. You will have to supply your unique API Key to a [`carto.Client`](#cartoclient). + +```javascript +var client = new carto.Client({ + apiKey: 'YOUR_API_KEY_HERE', + username: 'YOUR_USERNAME_HERE' +}); +``` diff --git a/docs/reference/03-versioning.md b/docs/reference/03-versioning.md new file mode 100644 index 0000000..40954e6 --- /dev/null +++ b/docs/reference/03-versioning.md @@ -0,0 +1,10 @@ +## Versioning + +CARTO.js uses [Semantic Versioning](http://semver.org/). View our Github repository to find tags for each [release](https://github.com/CartoDB/carto.js/releases). + +To get the version number programmatically, use `carto.version`. + +```javascript +console.log(carto.version); +// returns the version of the library +``` diff --git a/docs/reference/04-loading-the-library.md b/docs/reference/04-loading-the-library.md new file mode 100644 index 0000000..9428dd4 --- /dev/null +++ b/docs/reference/04-loading-the-library.md @@ -0,0 +1,20 @@ +## Loading the Library +CARTO.js is hosted on a CDN for easy loading. You can load the full source "carto.js" file or the minified version "carto.min.js". Once the script is loaded, you will have a global `carto` namespace. +CARTO.js is hosted in NPM as well. You can require it as a dependency in your custom apps. + +```html + + + + + +``` + +```javascript +// NPM: load the latest CARTO.js version +npm install @carto/carto.js +// or +yarn add @carto/carto.js + +var carto = require('@carto/carto.js'); +``` diff --git a/docs/reference/05-error-handling.md b/docs/reference/05-error-handling.md new file mode 100644 index 0000000..3223107 --- /dev/null +++ b/docs/reference/05-error-handling.md @@ -0,0 +1,20 @@ +## Error Handling + +Most of the errors fired by the library are handled by the client itself. The client will trigger a `CartoError` every time an error happens. + +A cartoError is an object containing a single `message` field with a string explaining the error. + +Some methods in CARTO.js are asynchronous. This means that they return a promise that will be fulfilled when the asynchronous work is done or rejected with a `CartoError` when an error occurs. + + +```javascript +// All errors are passed to the client. +client.on(carto.events.ERROR, cartoError => { + console.error(cartoError.message): +}) + +// .addLayer() is async. +client.addLayer(newLayer) + .then(successCallback) + .catch(errorCallback); +``` diff --git a/docs/support/01-support-options.md b/docs/support/01-support-options.md new file mode 100644 index 0000000..ab64767 --- /dev/null +++ b/docs/support/01-support-options.md @@ -0,0 +1,38 @@ +## Support Options + +Feeling stuck? There are many ways to find help. + +* Ask a question on [GIS StackExchange](https://gis.stackexchange.com/questions/tagged/carto) using the `CARTO` tag. +* [Report an issue](https://github.com/CartoDB/carto.js/issues) in Github. +* Enterprise Plan customers have additional access to enterprise-level support through CARTO's support representatives. + +If you just want to describe an issue or share an idea, just . + +### Issues on Github + +If you think you may have found a bug, or if you have a feature request that you would like to share with the CARTO.js team, please [open an issue](https://github.com/cartodb/carto.js/issues/new). + +Before opening an issue, review the [contributing guidelines](https://github.com/CartoDB/carto.js/blob/develop/CONTRIBUTING.md#filling-a-ticket). + + +### Community support on GIS Stack Exchange + +GIS Stack Exchange is the most popular community in the geospatial industry. This is a collaboratively-edited question and answer site for geospatial programmers and technicians. It is a fantastic resource for asking technical questions about developing and maintaining your application. + +Members of the CARTO.js team regularly monitor the `carto` tag. You can look for CARTO topics by adding `carto` or `carto.js` to your search query. You can also add additional tags to your question in order to attract the attention of experts in related technologies. + + +When posting a new question, please consider the following: + +* Read the GIS Stack Exchange [help](https://gis.stackexchange.com/help) and [how to ask](https://gis.stackexchange.com/help/how-to-ask) pages for guidelines and tips about posting questions. +* Be very clear about your question in the subject. A clear explanation helps those trying to answer your question, as well as those who may be looking for information in the future. +* Be informative in your post. Details, code snippets, logs, screenshots, etc. help others to understand your problem. +* Use code that demonstrates the problem. It is very hard to debug errors without sample code to reproduce the problem. + +### Enterprise Plan Customers + +Enterprise Plan customers have additional support options beyond general community support. As per your account Terms of Service, you have access to enterprise-level support through CARTO's support representatives available at [enterprise-support@carto.com](mailto:enterprise-support@carto.com) + +In order to speed up the resolution of your issue, provide as much information as possible (even if it is a link from community support). This allows our engineers to investigate your problem as soon as possible. + +If you are not yet CARTO customer, browse our [plans & pricing](https://carto.com/pricing/) and find the right plan for you. diff --git a/docs/support/02-faq.md b/docs/support/02-faq.md new file mode 100644 index 0000000..8be4d2f --- /dev/null +++ b/docs/support/02-faq.md @@ -0,0 +1,51 @@ +## FAQs + +CARTO.js v4 introduces new concepts that in some cases, change the behavior of old components. This section clarifies those changes and describe new functionality. + +### Do I need an API Key? + +Yes. See the guide _[Get API Key]({{site.cartojs_docs}}/guides/get-api-key/)_ or the [full reference API]({{site.cartojs_docs}}/reference/#authentication) for details. + +If you want to learn more about authorization and authentication in the CARTO Platform, read the [fundamentals]({{site.fundamental_docs}}/authorization/) about this topic, or dig into the [Auth API]({{site.authapi_docs}}/) details. + +### How do I pay for my API Keys? + +All billing is managed through your CARTO account. + +The release of the updated CARTO.js library does not impact your plan in any way. There are no changes in terms of costs or payments of your CARTO use. + +### What are the main features of this release? + +This new library allows you to create custom Location Intelligence applications using Builder capabilities. + +Highlights of this release include the ability to: + +1. Display data from datasets managed in Builder on a Leaflet map. +2. Manage layers programmatically. +3. Get data from CARTO to create custom UI components using Dataviews. + +You can learn more about these main features reading the [final release announcement]({{site.cartojs_docs}}/support/release-announcement/). + +### Where's the repository now? + +The repo has been renamed and moved to https://github.com/CartoDB/carto.js. + +### Does CARTO.js support Named Maps? + +No. + +Named Maps was a powerful and popular feature yet not easy to understand. Named maps were designed as a workaround for some of the limitations of our API keys. Named maps' purpose might be replicated by the new authorization system. + +### Can I still use `cartodb.createVis` and `viz.json` URLs? + +No, you cannot. + +While `cartodb.createVis` and `viz.json` URLs were convenient and allowed you to prototype a map using CARTO Editor (the former version of Builder) to create a custom app, the functionality of the library has changed. + +In CARTO.js v4, we are taking a more programmatic, low-level approach and identifying components that are separate from the front-end tool (CARTO Builder). This will make the API easier to use and more intuitive, giving developers greater flexibility in maintaining their core application. + +### Is CartoDB.js v.3.15 still available? + +Yes. + +Both the [source code](https://github.com/CartoDB/carto.js/tree/v3.15.14) and the [documentation](https://carto.com/docs/carto-engine/carto-js/) are still available. diff --git a/docs/support/03-error-messages.md b/docs/support/03-error-messages.md new file mode 100644 index 0000000..268fa54 --- /dev/null +++ b/docs/support/03-error-messages.md @@ -0,0 +1,372 @@ +## Errors + +CARTO.js emits error objects when something goes wrong. Errors appear in your developer console if not caught. The error object has a code and a description to help you identify the problem and troubleshoot. + +### CARTO.js API Error Codes + +If you encounter an error while loading CARTO.js, the following table contains a list of known errors codes and possible solutions. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Error Code
Message
api-key-required +

apiKey property is required.

+
api-key-string +

apiKey property must be a string.

+
username-required +

username property is required.

+
username-string +

username property must be a string.

+
non-valid-server-url +

serverUrl is not a valid URL.

+
non-matching-server-url +

serverUrl doesn't match the username.

+
+ + +### CARTO.js Error Codes + +If you find a validation error on Chrome JavaScript Console, Firefox Web Console, or any other equivalent tools in your browser, please reference the tables below to find explanations for the validation errors. Each table gives specific error information for the different components of CARTO.js. + + +### Dataview + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Error Code
Message
source-required +

Source property is required.

+
column-required +

Column property is required.

+
column-string +

Column property must be a string.

+
empty-column +

Column property must be not empty.

+
filter-required +

Filter property is required.

+
time-series-options-required +

Options object to create a time series dataview is required.

+
time-series-invalid-aggregation +

Time aggregation must be a valid value. Use carto.dataview.timeAggregation.

+
time-series-invalid-offset +

Offset must an integer value between -12 and 14.

+
time-series-invalid-uselocaltimezone +

useLocalTimezone must be a boolean value.

+
histogram-options-required +

Options object to create a histogram dataview is required.

+
histogram-invalid-bins +

Bins must be a positive integer value.

+
formula-options-required +

Formula dataview options are not defined.

+
formula-invalid-operation +

Operation for formula dataview is not valid. Use carto.operation.

+
category-options-required +

Category dataview options are not defined.

+
category-limit-required +

Limit for category dataview is required.

+
category-limit-number +

Limit for category dataview must be a number.

+
category-limit-positive +

Limit for category dataview must be greater than 0.

+
category-invalid-operation +

Operation for category dataview is not valid. Use carto.operation.

+
category-operation-required +

Operation column for category dataview is required.

+
category-operation-string +

Operation column for category dataview must be a string.

+
category-operation-empty +

Operation column for category dataview must be not empty.

+
+ + +### Filter + + + + + + + + + + + + + + +
Error Code
Message
invalid-bounds-object +

Bounds object is not valid. Use a carto.filter.Bounds object.

+
+ +### Layer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Error Code
Message
non-valid-source +

The given object is not a valid source. See carto.source.Base.

+
bad-layer-type +

The given object is not a layer.

+
non-valid-style +

The given object is not a valid style. See carto.style.Base.

+
source-with-different-client +

A layer can't have a source which belongs to a different client.

+
style-with-different-client +

A layer can't have a style which belongs to a different client.

+
+ +### Source + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Error Code
Message
query-required +

SQL Source must have a SQL query.

+
query-string +

SQL Query must be a string.

+
no-dataset-name +

Table name is required.

+
dataset-string +

Table name must be a string.

+
dataset-required +

Table name must be a string.

+
+ +### Style + + + + + + + + + + + + + + + + + + +
Error Code
Message
required-css +

CartoCSS is required.

+
css-string +

CartoCSS must be a string.

+
+ +### CARTO.js Platform Error Codes for Developers + +If you find an error on Chrome JavaScript Console, Firefox Web Console, or any other equivalent tools on your browsers (regarding platform (aka. backend services)), please reference the table below to find explanations for the error codes. + + + + + + + + + + + + + + + + + + + + + +
Error Code
Message
Description
over-platform-limits +

You are over platform's limits.

+
+

In order to guarantee the performance of those APIs for every user of the CARTO platform and prevent abuse, we have set up some general limitations and restrictions on how they work.

+ +        

Learn about fundamentals of [limits]({{site.fundamental_docs}}/limits/) in CARTO.

+
generic-limit-error +

The server is taking too long to respond.

+

Due to poor conectivity or a temporary error with our servers, we cannot handle your request. Please try again soon.

+ +### Checking Errors in your Browser + +CARTO.js writes error messages to `window.console` if they are not caught. Please check the developer documentation for your browser in order to understand how you can check it. + +If any errors occurred when loading CARTO.js, they appear as one or more lines in the console. You can check the [error codes table](#errors above to find te error in the error message. You can also find the details about the error message in the [Full Reference API]({{site.cartojs_docs}}/reference/). + +Ensure that you are using a [supported browser]({{site.cartojs_docs}}/support/browsers/). diff --git a/docs/support/04-browser-support.md b/docs/support/04-browser-support.md new file mode 100644 index 0000000..eb9979c --- /dev/null +++ b/docs/support/04-browser-support.md @@ -0,0 +1,15 @@ +## Browser Support + +CARTO.js works for the last 2 versions in any modern browser and IE11. + +### Desktop Browsers + +| ![Chrome](https://camo.githubusercontent.com/6bb46f736c023ca3a96263d88cc30ea94a19bd3f/68747470733a2f2f63646e6a732e636c6f7564666c6172652e636f6d2f616a61782f6c6962732f62726f777365722d6c6f676f732f33392e332e302f617263686976652f6368726f6d655f31322d34382f6368726f6d655f31322d34385f34387834382e706e67) | ![Firefox](https://camo.githubusercontent.com/a2f2aef3a61ac88d31266cc5745c92a0943a51fd/68747470733a2f2f63646e6a732e636c6f7564666c6172652e636f6d2f616a61782f6c6962732f62726f777365722d6c6f676f732f33392e332e302f617263686976652f66697265666f785f312e352d332f66697265666f785f312e352d335f34387834382e706e67) | ![Edge](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/45.10.0/edge/edge_48x48.png) | ![Opera](https://camo.githubusercontent.com/f8b46198612edbe858c531588c179a9a564690cc/68747470733a2f2f63646e6a732e636c6f7564666c6172652e636f6d2f616a61782f6c6962732f62726f777365722d6c6f676f732f33392e332e302f6f706572612f6f706572615f34387834382e706e67) | ![Safari](https://camo.githubusercontent.com/bfd047508bfa78a389415ab494cc8dcc04d94e7d/68747470733a2f2f63646e6a732e636c6f7564666c6172652e636f6d2f616a61782f6c6962732f62726f777365722d6c6f676f732f33392e332e302f7361666172692f7361666172695f34387834382e706e67) | ![IE](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/45.10.0/archive/internet-explorer_9-11/internet-explorer_9-11_48x48.png) | +|:-------------:|:-------------:|:-----:|:-------------:|:-----:|:-----:| +| Chrome | Mozilla | Edge | Opera | Safari | IE 11 | + +### Mobile Browsers + +| ![Chrome](https://camo.githubusercontent.com/6bb46f736c023ca3a96263d88cc30ea94a19bd3f/68747470733a2f2f63646e6a732e636c6f7564666c6172652e636f6d2f616a61782f6c6962732f62726f777365722d6c6f676f732f33392e332e302f617263686976652f6368726f6d655f31322d34382f6368726f6d655f31322d34385f34387834382e706e67) | ![Safari](https://camo.githubusercontent.com/bfd047508bfa78a389415ab494cc8dcc04d94e7d/68747470733a2f2f63646e6a732e636c6f7564666c6172652e636f6d2f616a61782f6c6962732f62726f777365722d6c6f676f732f33392e332e302f7361666172692f7361666172695f34387834382e706e67) | ![Samsung Internet](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/45.10.0/archive/samsung-internet_5/samsung-internet_5_48x48.png) | +|:-------------:|:-------------:|:-------------:| +| Mobile Chrome | Mobile Safari | Samsung Internet | diff --git a/docs/support/05-release-announcement.md b/docs/support/05-release-announcement.md new file mode 100644 index 0000000..261d2bf --- /dev/null +++ b/docs/support/05-release-announcement.md @@ -0,0 +1,17 @@ +## v4 Release Announcement + +After several months of hard work, CARTO is very proud to announce the __final release of CARTO.js v4__, which replaces our existing CartoDB.js library. We recognize that you have been anticipating an updated version for some time so before we give you the details, thank you for your patience! + +The updated CARTO.js library has been __rebuilt from the ground up__. Case Studies indicated that we could change how we structured the API to make it more intuitive for developers. As a result, we created __a more low-level, programmatic approach__ which makes it easier to use and allows you to create fully customized location intelligence apps. + +Some of the key features of the Beta release are: + +* __A new way of displaying data__ hosted on your CARTO account, on top of Leaflet and Google Maps maps, in the form of layers. +* __A mechanism for extracting data__ from your CARTO account in predefined ways (eg: a list of categories). This is the same mechanism that Builder uses internally for its powerful widgets. It includes being able to filter this data by a bounding box. +* __A way of getting the metadata__ associated to the styles of a particular layer. For example, you will be able to get the names and colors of the categories for a layer styled using a color scheme. + +We know that documentation is critical for any good JS library and used this as an opportunity to begin the redesign of how we provide documentation for all of our CARTO platform. Browse the interactive API documentation for the final release to search for specific CARTO.js methods, arguments, and sample code that can be used to build your applications. + +Please use the final release and The goal is to gather as much feedback as possible so that we can ensure CARTO.js v4 is rock-solid and super useful for all CARTO users. You can find different [support-options]({{site.cartojs_docs}}/support) if you need help. + +__Happy coding!__ diff --git a/docs/support/06-contribute.md b/docs/support/06-contribute.md new file mode 100644 index 0000000..4f464e9 --- /dev/null +++ b/docs/support/06-contribute.md @@ -0,0 +1,36 @@ +## Contribute + +CARTO platform is an open-source ecosystem. You can read about the [fundamentals]({{site.fundamental_docs}}/components/) of CARTO architecture and its components. +We are more than happy to receive your contributions to the code and the documentation as well. + +## Filling a ticket +If you want to open a new issue in our repository, please follow these instructions: + +1. Descriptive title. +2. Write a good description, it always helps. +3. Include your browser, OS and CARTO.js version (it shows up in the browser console). +4. Specify the steps to reproduce the problem. +5. Try to add an example showing the problem (using [JSFiddle](http://jsfiddle.net), [JSBin](http://jsbin.com),...). + + +## Contributing code +Best part of open source, collaborate in CARTO.js code!. We like hearing from you, so if you have any bug fixed, or a new feature ready to be merged, those are the steps you should follow: + +1. Fork the CARTO.js repository. +2. Create a new branch in your forked repository. +3. Commit your changes. Add new tests if it is necessary (```grunt test```), remember to follow ["How to build"](https://github.com/CartoDB/carto.js/blob/master/README.md#how-to-build) steps. +4. Open a pull request. +5. Any of the CARTO.js mantainers will take a look. +6. If everything works, it will merged and released \o/. + +If you want more detailed information, this [GitHub guide](https://guides.github.com/activities/contributing-to-open-source/) is a must. + + +## Completing documentation + +CARTO.js documentation is located in ```docs/```. That folder is the content that appears in the [Developer Center](http://carto.com/developer-center/carto-js/). +Just follow the instructions described in [contributing code](#contributing-code) and after accepting your pull request, we will make it appear online :). + +## Submitting contributions + +You will need to sign a Contributor License Agreement (CLA) before making a submission. [Learn more here](https://carto.com/contributions). diff --git a/examples/acceptance/displaced-popups.html b/examples/acceptance/displaced-popups.html new file mode 100644 index 0000000..0a348ce --- /dev/null +++ b/examples/acceptance/displaced-popups.html @@ -0,0 +1,109 @@ + + + Refactoria + + + + + + + + + + + + + + + Popups should position correctly even if page is scrolled +

+
+ + + + diff --git a/examples/acceptance/multiclient-gmaps.html b/examples/acceptance/multiclient-gmaps.html new file mode 100644 index 0000000..7b88527 --- /dev/null +++ b/examples/acceptance/multiclient-gmaps.html @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + +
+ + + + + diff --git a/examples/examples.json b/examples/examples.json new file mode 100644 index 0000000..da55743 --- /dev/null +++ b/examples/examples.json @@ -0,0 +1,268 @@ +{ + "main": { + "file": { + "leaflet": "public/layers/change-order-leaflet.html", + "gmaps": "public/layers/change-order-gmaps.html" + } + }, + "categories": [ + { + "title": "Basics", + "samples": [ + { + "title": "Add a layer", + "desc": "Add one CARTO layer to your map", + "file": { + "leaflet": "public/layers/single-layer-leaflet.html", + "gmaps": "public/layers/single-layer-gmaps.html" + } + }, + { + "title": "Add more layers", + "desc": "Add multiple CARTO layers to your map", + "file": { + "leaflet": "public/layers/multilayer-leaflet.html", + "gmaps": "public/layers/multilayer-gmaps.html" + } + }, + { + "title": "Change the source", + "desc": "Update the source of your layers", + "file": { + "leaflet": "public/layers/change-source-leaflet.html", + "gmaps": "public/layers/change-source-gmaps.html" + } + }, + { + "title": "Change the style", + "desc": "Update the style of your layers", + "file": { + "leaflet": "public/layers/change-style-leaflet.html", + "gmaps": "public/layers/change-style-gmaps.html" + } + }, + { + "title": "Move the layers", + "desc": "Update the order of your layers", + "file": { + "leaflet": "public/layers/change-order-leaflet.html", + "gmaps": "public/layers/change-order-gmaps.html" + } + }, + { + "title": "Server tile aggregation", + "desc": "Apply smart backend aggregation", + "file": { + "leaflet": "public/layers/layer-with-aggregation-leaflet.html", + "gmaps": "public/layers/layer-with-aggregation-gmaps.html" + } + }, + { + "title": "Server tile aggregation with cluster of points", + "desc": "Apply smart backend aggregation to create a cluster of points", + "file": { + "leaflet": "public/layers/layer-with-aggregation-cluster-leaflet.html", + "gmaps": "public/layers/layer-with-aggregation-cluster-gmaps.html" + } + } + ] + }, + { + "title": "Interactivity", + "samples": [ + { + "title": "Detect feature click", + "desc": "Interact with the features on the click event", + "file": { + "leaflet": "public/layers/feature-click-leaflet.html", + "gmaps": "public/layers/feature-click-gmaps.html" + } + }, + { + "title": "Detect feature over/out", + "desc": "Interact with the features on the over/out events", + "file": { + "leaflet": "public/layers/feature-over-out-leaflet.html", + "gmaps": "public/layers/feature-over-out-gmaps.html" + } + }, + { + "title": "Change the feature columns", + "desc": "Change the columns returned in the feature event", + "file": { + "leaflet": "public/layers/change-feature-columns-leaflet.html", + "gmaps": "public/layers/change-feature-columns-gmaps.html" + } + } + ] + }, + { + "title": "Widgets", + "samples": [ + { + "title": "Formula widget", + "desc": "Create a widget with the formula dataview", + "file": "public/dataviews/formula.html" + }, + { + "title": "Category widget", + "desc": "Create a widget with the category dataview", + "file": "public/dataviews/category.html" + }, + { + "title": "Histogram widget", + "desc": "Create a widget with the histogram dataview", + "file": "public/dataviews/histogram.html" + }, + { + "title": "Time Series widget", + "desc": "Create a widget with the time series dataview", + "file": "public/dataviews/time-series.html" + }, + { + "title": "Bounding Box filter", + "desc": "Apply a map bounding box filter to the widgets", + "file": { + "leaflet": "public/filters/bounding-box-leaflet.html", + "gmaps": "public/filters/bounding-box-gmaps.html" + } + }, + { + "title": "Circle filter", + "desc": "Apply a circle filter to the widgets", + "file": "public/filters/circle-filter.html" + }, + { + "title": "Polygon filter", + "desc": "Apply a polygon filter to the widgets", + "file": "public/filters/polygon-filter.html" + }, + { + "title": "Status and error handling", + "desc": "Manage the status and erros in dataviews", + "file": "public/dataviews/error-handling.html" + } + ] + }, + { + "title": "Filters", + "samples": [ + { + "title": "Category Filter", + "desc": "Create a Category filter with checkbox selectors", + "file": "public/filters/category-filter.html" + }, + { + "title": "Range Filter", + "desc": "Create a Range filter with a range slider", + "file": "public/filters/range-filter.html" + }, + { + "title": "AND Filter", + "desc": "Apply an AND filter to combine multiple filters", + "file": "public/filters/and-filter.html" + }, + { + "title": "Complex Filter", + "desc": "Apply several combined filters to a dataset", + "file": "public/filters/complex-filter.html" + } + ] + }, + { + "title": "Misc", + "samples": [ + { + "title": "Guide", + "desc": "Full reference guide example", + "file": { + "leaflet": "public/misc/guide-leaflet.html", + "gmaps": "public/misc/guide-gmaps.html" + } + }, + { + "title": "Pop-Ups", + "desc": "Create pop-up information windows and interact with your map", + "file": { + "leaflet": "public/misc/popups-leaflet.html", + "gmaps": "public/misc/popups-gmaps.html" + } + }, + { + "title": "Pop-Ups with embed videos", + "desc": "Create pop-up information windows with embed videos.", + "file": { + "leaflet": "public/misc/popups-video-leaflet.html", + "gmaps": "public/misc/popups-video-gmaps.html" + } + }, + { + "title": "Legends", + "desc": "Create dynamic legends", + "file": { + "leaflet": "public/misc/legends-leaflet.html", + "gmaps": "public/misc/legends-gmaps.html" + } + }, + { + "title": "Most/less populated places", + "desc": "Filter the 20 most/less populated places in the world", + "file": { + "leaflet": "public/misc/populated-places-leaflet.html", + "gmaps": "public/misc/populated-places-gmaps.html" + } + }, + { + "title": "Edit SQL & CartoCSS", + "desc": "Edit manually the SQL query and the CartoCSS style", + "file": { + "leaflet": "public/misc/edit-sql-cartocss-leaflet.html", + "gmaps": "public/misc/edit-sql-cartocss-gmaps.html" + } + }, + { + "title": "Error handling", + "desc": "Handle common errors in maps", + "file": { + "leaflet": "public/misc/error-handling-leaflet.html", + "gmaps": "public/misc/error-handling-gmaps.html" + } + }, + { + "title": "Hexagon aggregation", + "desc": "Aggregate point data into hexagon grid", + "file": { + "leaflet": "public/misc/hexagon-aggregation-leaflet.html", + "gmaps": "public/misc/hexagon-aggregation-gmaps.html" + } + }, + { + "title": "Filter data using drawing tools", + "desc": "Filter data based on a drawn circle", + "file": { + "leaflet": "public/misc/filter-data-map-leaflet.html", + "gmaps": "public/misc/filter-data-map-gmaps.html" + } + }, + { + "title": "Dropdown menu with data from CARTO dataset", + "desc": "Look for data within your dataset using dropdown menu.", + "file": { + "leaflet": "public/misc/custom-search-dataset-leaflet.html", + "gmaps": "public/misc/custom-search-dataset-gmaps.html" + } + }, + { + "title": "Storymap using CARTO.js and Storymap.js", + "desc": "Create a storytelling map using Storymap.js", + "file": "public/misc/storymap-leaflet.html" + }, + { + "title": "Pie Chart Widget using CARTO.js and Vega-Lite", + "desc": "Create a map with a pie chart using Vega Lite and CARTO.js category dataviews.", + "file": "public/misc/piechart-vega.html" + } + ] + } + ] +} diff --git a/examples/index.html b/examples/index.html new file mode 100644 index 0000000..d68c4fa --- /dev/null +++ b/examples/index.html @@ -0,0 +1,44 @@ + + + + + Examples | CARTO + + + + +

Examples

+
+

CDN: ../dist/public/carto.js

+ + + diff --git a/examples/internal/basemap-selector.html b/examples/internal/basemap-selector.html new file mode 100644 index 0000000..ef90435 --- /dev/null +++ b/examples/internal/basemap-selector.html @@ -0,0 +1,351 @@ + + + + + Basemap selector | CARTO + + + + + + + + + + + + +
+ +
+

Basemap selector

+

CartoDB

+ +

Google Maps

+ +
+ + + + + + + + + + + diff --git a/examples/internal/gmaps-geometry-editor.html b/examples/internal/gmaps-geometry-editor.html new file mode 100644 index 0000000..4e3b1f3 --- /dev/null +++ b/examples/internal/gmaps-geometry-editor.html @@ -0,0 +1,596 @@ + + + + + Geometry Editor | CARTO + + + + + + + + + + + + + + +
+ + + + + + + + + + + diff --git a/examples/internal/leaflet-geometry-editor.html b/examples/internal/leaflet-geometry-editor.html new file mode 100644 index 0000000..2508c42 --- /dev/null +++ b/examples/internal/leaflet-geometry-editor.html @@ -0,0 +1,607 @@ + + + + Geometry Editor | CARTO + + + + + + + + + + + + + +
+ + + + + + + diff --git a/examples/internal/leaflet-vector.html b/examples/internal/leaflet-vector.html new file mode 100644 index 0000000..b1a7fbc --- /dev/null +++ b/examples/internal/leaflet-vector.html @@ -0,0 +1,269 @@ + + + + Easy example | CARTO + + + + + + + + + + +
+ + + + + + + diff --git a/examples/internal/search-geocode.html b/examples/internal/search-geocode.html new file mode 100644 index 0000000..0ad53cd --- /dev/null +++ b/examples/internal/search-geocode.html @@ -0,0 +1,77 @@ + + + + + + + + Base example | CARTO + + + + + + + + + + + + + + + + +
+ + + diff --git a/examples/internal/viz/000.json b/examples/internal/viz/000.json new file mode 100644 index 0000000..e53534b --- /dev/null +++ b/examples/internal/viz/000.json @@ -0,0 +1,203 @@ +{ + "id": "2b13c956-e7c1-11e2-806b-5404a6a683d5", + "version": "0.1.0", + "title": "european_countries_e 0", + "likes": 0, + "description": null, + "scrollwheel": true, + "legends": true, + "url": null, + "map_provider": "leaflet", + "center": "[52.5897007687178, 52.734375]", + "zoom": 4, + "updated_at": "2015-03-13T11:24:37+00:00", + "datasource": { + "user_name": "documentation", + "maps_api_template": "http://{user}.cartodb.com:80", + "force_cors": true, + "stat_tag": "84ec6844-4b4b-11e5-9c1d-080027880ca6" + }, + "layers": [ + { + "options": { + "id": "0a3d9104-99c6-482b-9f8c-7c6134bddcdc", + "order": 0, + "visible": true, + "type": "Tiled", + "name": "Positron", + "className": "default positron_rainbow", + "baseType": "positron_rainbow", + "urlTemplate": "http://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png", + "read_only": true, + "minZoom": "0", + "maxZoom": "18", + "attribution": "© OpenStreetMap contributors", + "subdomains": "abcd", + "category": "CartoDB" + }, + "infowindow": null, + "tooltip": null, + "id": "0a3d9104-99c6-482b-9f8c-7c6134bddcdc", + "order": 0, + "type": "tiled" + }, + { + "id": "923b7812-2d56-41c6-ac15-3ce430db090f", + "type": "CartoDB", + "infowindow": { + "fields": [ + { + "name": "name", + "title": true, + "position": 4 + } + ], + "template": "
{{ name }}
", + "alternative_names": {}, + "width": 226, + "maxHeight": 180 + }, + "tooltip": { + "fields": [ + { + "name": "name", + "title": true, + "position": 1 + } + ], + "template_name": "tooltip_light", + "template": "
TEMPLATE_1
", + "alternative_names": {}, + "maxHeight": 180 + }, + "legend": { + "type": "choropleth", + "show_title": false, + "title": "", + "template": "", + "items": [ + { + "name": "Left label", + "visible": true, + "value": "5592.00", + "type": "text" + }, + { + "name": "Right label", + "visible": true, + "value": "1638094.00", + "type": "text" + }, + { + "name": "Color", + "visible": true, + "value": "#FFFFB2", + "type": "color" + }, + { + "name": "Color", + "visible": true, + "value": "#FED976", + "type": "color" + }, + { + "name": "Color", + "visible": true, + "value": "#FEB24C", + "type": "color" + }, + { + "name": "Color", + "visible": true, + "value": "#FD8D3C", + "type": "color" + }, + { + "name": "Color", + "visible": true, + "value": "#FC4E2A", + "type": "color" + }, + { + "name": "Color", + "visible": true, + "value": "#E31A1C", + "type": "color" + }, + { + "name": "Color", + "visible": true, + "value": "#B10026", + "type": "color" + } + ] + }, + "order": 1, + "visible": true, + "options": { + "sql": "select * from european_countries_e", + "layer_name": "european_countries_e", + "cartocss": "/** choropleth visualization */\n\n#european_countries_e{\n polygon-fill: #FFFFB2;\n polygon-opacity: 0.8;\n line-color: #FFF;\n line-width: 1;\n line-opacity: 0.5;\n}\n#european_countries_e [ area <= 1638094] {\n polygon-fill: #B10026;\n}\n#european_countries_e [ area <= 55010] {\n polygon-fill: #E31A1C;\n}\n#european_countries_e [ area <= 34895] {\n polygon-fill: #FC4E2A;\n}\n#european_countries_e [ area <= 12890] {\n polygon-fill: #FD8D3C;\n}\n#european_countries_e [ area <= 10025] {\n polygon-fill: #FEB24C;\n}\n#european_countries_e [ area <= 9150] {\n polygon-fill: #FED976;\n}\n#european_countries_e [ area <= 5592] {\n polygon-fill: #FFFFB2;\n}", + "cartocss_version": "2.1.1", + "table_name": "\"\"." + } + }, + { + "options": { + "visible": true, + "type": "Tiled", + "default": "true", + "url": "http://{s}.basemaps.cartocdn.com/light_only_labels/{z}/{x}/{y}.png", + "subdomains": "abcd", + "minZoom": "0", + "maxZoom": "18", + "attribution": "© OpenStreetMap contributors", + "urlTemplate": "http://{s}.basemaps.cartocdn.com/light_only_labels/{z}/{x}/{y}.png", + "name": "Positron Labels", + "id": "bc054932-1ae8-4d4a-96d6-d49ce4247a59", + "className": "httpsbasemapscartocdncomlight_only_labelszxypng", + "order": 3 + }, + "infowindow": null, + "tooltip": null, + "id": "bc054932-1ae8-4d4a-96d6-d49ce4247a59", + "order": 3, + "type": "tiled" + } + ], + "overlays": [ + { + "type": "loader", + "options": { + "display": true, + "x": 20, + "y": 150 + } + }, + { + "type": "fullscreen", + "options": { + "x": 20, + "y": 140, + "display": true + } + }, + { + "type": "search", + "options": { + "display": true + } + }, + { + "type": "zoom", + "options": { + "x": 20, + "y": 20, + "display": true + } + } + ], + "prev": null, + "next": null, + "transition_options": {} +} diff --git a/examples/internal/viz/001.json b/examples/internal/viz/001.json new file mode 100644 index 0000000..955663e --- /dev/null +++ b/examples/internal/viz/001.json @@ -0,0 +1,170 @@ +{ + "bounds": [ + [ + -73.8273, + -179 + ], + [ + 83.261, + 179 + ] + ], + "center": "[41, -3]", + "datasource": { + "user_name": "cartojs-test", + "maps_api_template": "https://{user}.carto.com:443", + "stat_tag": "3626e3ca-1e40-4e5c-95f8-f83b54131ddc" + }, + "description": "

Basic test displaying points in red.

\n", + "options": { + "dashboard_menu": true, + "layer_selector": false, + "legends": true, + "scrollwheel": true + }, + "id": "3626e3ca-1e40-4e5c-95f8-f83b54131ddc", + "layers": [ + { + "id": "e7bbdf2c-8e66-49f3-b7b8-793e3a4fd6ea", + "type": "tiled", + "options": { + "default": "true", + "url": "https://cartodb-basemaps-{s}.global.ssl.fastly.net/rastertiles/voyager_nolabels/{z}/{x}/{y}.png", + "subdomains": "abcd", + "minZoom": "0", + "maxZoom": "18", + "name": "Voyager", + "className": "voyager_labels", + "attribution": "© OpenStreetMap contributors", + "labels": { + "url": "https://cartodb-basemaps-{s}.global.ssl.fastly.net/rastertiles/voyager_only_labels/{z}/{x}/{y}.png" + }, + "urlTemplate": "https://cartodb-basemaps-{s}.global.ssl.fastly.net/rastertiles/voyager_nolabels/{z}/{x}/{y}.png" + } + }, + { + "id": "b39d9e30-1a03-48d2-8734-4e700632f800", + "legends": [], + "type": "CartoDB", + "infowindow": { + "template_name": "none", + "fields": [], + "maxHeight": 180, + "template": "", + "alternative_names": {}, + "width": 226, + "headerColor": { + "color": { + "fixed": "#35AAE5", + "opacity": 1 + } + } + }, + "tooltip": { + "fields": [], + "template_name": "none", + "template": "", + "template_type": "mustache" + }, + "visible": true, + "options": { + "layer_name": "ne_10m_populated_places_simple", + "cartocss": "#layer {\n marker-width: 10;\n marker-fill: #ff0900;\n marker-fill-opacity: 1;\n marker-allow-overlap: true;\n marker-line-width: 1;\n marker-line-color: #FFFFFF;\n marker-line-opacity: 1;\n}", + "cartocss_version": "2.1.1", + "attribution": "", + "source": "a0" + } + }, + { + "id": "7a5e026f-d014-43a7-b0c3-403eaabc889d", + "type": "tiled", + "options": { + "default": "true", + "url": "https://cartodb-basemaps-{s}.global.ssl.fastly.net/rastertiles/voyager_only_labels/{z}/{x}/{y}.png", + "subdomains": "abcd", + "minZoom": "0", + "maxZoom": "18", + "attribution": "© OpenStreetMap contributors", + "urlTemplate": "https://cartodb-basemaps-{s}.global.ssl.fastly.net/rastertiles/voyager_only_labels/{z}/{x}/{y}.png", + "type": "Tiled", + "name": "Voyager Labels" + } + } + ], + "likes": 0, + "map_provider": "leaflet", + "overlays": [ + { + "type": "share", + "order": 2, + "options": { + "display": true, + "x": 20, + "y": 20 + }, + "template": "" + }, + { + "type": "search", + "order": 3, + "options": { + "display": true, + "x": 60, + "y": 20 + }, + "template": "" + }, + { + "type": "zoom", + "order": 6, + "options": { + "display": true, + "x": 20, + "y": 20 + }, + "template": "+ -" + }, + { + "type": "loader", + "order": 8, + "options": { + "display": true, + "x": 20, + "y": 150 + }, + "template": "
" + }, + { + "type": "logo", + "order": 9, + "options": { + "display": true, + "x": 10, + "y": 40 + }, + "template": "" + } + ], + "title": "001-Test", + "updated_at": "2017-10-24T11:29:21+00:00", + "user": { + "fullname": "cartojs-test", + "avatar_url": "//cartodb-libs.global.ssl.fastly.net/cartodbui/assets/unversioned/images/avatars/avatar_planet_orange.png", + "profile_url": "https://cartojs-test.carto.com" + }, + "version": "3.0.0", + "widgets": [], + "zoom": 7, + "analyses": [ + { + "id": "a0", + "type": "source", + "params": { + "query": "SELECT * FROM ne_10m_populated_places_simple" + }, + "options": { + "table_name": "ne_10m_populated_places_simple" + } + } + ] +} diff --git a/examples/internal/viz/002.json b/examples/internal/viz/002.json new file mode 100644 index 0000000..00803a6 --- /dev/null +++ b/examples/internal/viz/002.json @@ -0,0 +1,262 @@ +{ + "bounds": [ + [ + -73.8273, + -179 + ], + [ + 83.261, + 179 + ] + ], + "center": "[41, -3]", + "datasource": { + "user_name": "cartojs-test", + "maps_api_template": "https://{user}.carto.com:443", + "stat_tag": "b8c116e4-37af-4eee-83d6-79d4c33201e4" + }, + "description": "

Map to test the different generated legends.

\n\n

Points to test:

\n\n
    \n
  • Category legend
  • \n
  • Choropleth legend
  • \n
  • Bubble legend
  • \n
\n\n

(Choropleth and bubble share the same layer)

\n", + "options": { + "dashboard_menu": true, + "layer_selector": false, + "legends": true, + "scrollwheel": true + }, + "id": "b8c116e4-37af-4eee-83d6-79d4c33201e4", + "layers": [ + { + "id": "0176d48c-09b1-4bf2-9493-546b93e4df88", + "type": "tiled", + "options": { + "default": "true", + "url": "https://cartodb-basemaps-{s}.global.ssl.fastly.net/rastertiles/voyager_nolabels/{z}/{x}/{y}.png", + "subdomains": "abcd", + "minZoom": "0", + "maxZoom": "18", + "name": "Voyager", + "className": "voyager_labels", + "attribution": "© OpenStreetMap contributors", + "labels": { + "url": "https://cartodb-basemaps-{s}.global.ssl.fastly.net/rastertiles/voyager_only_labels/{z}/{x}/{y}.png" + }, + "urlTemplate": "https://cartodb-basemaps-{s}.global.ssl.fastly.net/rastertiles/voyager_nolabels/{z}/{x}/{y}.png", + "type": "Tiled" + } + }, + { + "id": "7c115d5b-6c1e-4390-890f-52d9c12f6cb2", + "legends": [ + { + "conf": { + "columns": [] + }, + "created_at": "2017-10-24T11:52:52+00:00", + "definition": {}, + "id": "61c56598-c76e-4b11-952e-7412452e6e29", + "layer_id": "7c115d5b-6c1e-4390-890f-52d9c12f6cb2", + "post_html": "", + "pre_html": "", + "title": "pop_max", + "type": "choropleth", + "updated_at": "2017-10-24T12:02:06+00:00" + }, + { + "conf": { + "columns": [] + }, + "created_at": "2017-10-24T12:01:27+00:00", + "definition": { + "color": "#fef6b5", + "top_label": "", + "bottom_label": "" + }, + "id": "6210c946-9ee5-4d8a-ba99-25a8d7c5bc65", + "layer_id": "7c115d5b-6c1e-4390-890f-52d9c12f6cb2", + "post_html": "", + "pre_html": "", + "title": "pop_max", + "type": "bubble", + "updated_at": "2017-10-24T12:02:06+00:00" + } + ], + "type": "CartoDB", + "infowindow": { + "template_name": "none", + "maxHeight": 180, + "template": "", + "alternative_names": {}, + "fields": null, + "width": 226, + "headerColor": { + "color": { + "fixed": "#35AAE5", + "opacity": 1 + } + } + }, + "tooltip": { + "template_name": "none", + "template": "" + }, + "visible": true, + "options": { + "layer_name": "Layer1", + "cartocss": "#layer {\n marker-width: ramp([pop_max], range(2, 11), quantiles(5));\n marker-fill: ramp([pop_max], (#fef6b5, #ffd08e, #ffa679, #f67b77, #e15383), quantiles);\n marker-fill-opacity: 1;\n marker-allow-overlap: true;\n marker-line-width: 1;\n marker-line-color: #FFFFFF;\n marker-line-opacity: 1;\n}", + "cartocss_version": "2.1.1", + "attribution": "", + "source": "c0" + } + }, + { + "id": "97bf19b9-fee0-49fd-ad44-8f4026da6888", + "legends": [ + { + "conf": { + "columns": [ + "title" + ] + }, + "created_at": "2017-10-24T11:52:00+00:00", + "definition": {}, + "id": "07cc33c9-63c3-4bc6-9c9a-6b3c46ebbda1", + "layer_id": "97bf19b9-fee0-49fd-ad44-8f4026da6888", + "post_html": "", + "pre_html": "", + "title": "Category Legend", + "type": "category", + "updated_at": "2017-10-24T11:59:25+00:00" + } + ], + "type": "CartoDB", + "infowindow": { + "template_name": "none", + "maxHeight": 180, + "template": "", + "alternative_names": {}, + "fields": null, + "width": 226, + "headerColor": { + "color": { + "fixed": "#35AAE5", + "opacity": 1 + } + } + }, + "tooltip": { + "template_name": "none", + "template": "" + }, + "visible": true, + "options": { + "layer_name": "layer0", + "cartocss": "#layer {\n marker-width: 7;\n marker-fill: ramp([featurecla], (#5F4690, #1D6996, #38A6A5, #0F8554, #73AF48, #EDAD08, #E17C05, #CC503E), (\"Populated place\", \"Admin-1 capital\", \"Admin-0 capital\", \"Admin-1 region capital\", \"Scientific station\", \"Admin-0 region capital\", \"Admin-0 capital alt\", \"Historic place\"), \"=\");\n marker-fill-opacity: 1;\n marker-allow-overlap: true;\n marker-line-width: 0;\n marker-line-color: #FFFFFF;\n marker-line-opacity: 1;\n}", + "cartocss_version": "2.1.1", + "attribution": "", + "source": "b0" + } + }, + { + "id": "2669e10c-d1bf-4ed5-8f8e-6518a1afb234", + "type": "tiled", + "options": { + "default": "true", + "url": "https://cartodb-basemaps-{s}.global.ssl.fastly.net/rastertiles/voyager_only_labels/{z}/{x}/{y}.png", + "subdomains": "abcd", + "minZoom": "0", + "maxZoom": "18", + "attribution": "© OpenStreetMap contributors", + "urlTemplate": "https://cartodb-basemaps-{s}.global.ssl.fastly.net/rastertiles/voyager_only_labels/{z}/{x}/{y}.png", + "type": "Tiled", + "name": "Voyager Labels" + } + } + ], + "likes": 0, + "map_provider": "leaflet", + "overlays": [ + { + "type": "share", + "order": 2, + "options": { + "display": true, + "x": 20, + "y": 20 + }, + "template": "" + }, + { + "type": "search", + "order": 3, + "options": { + "display": true, + "x": 60, + "y": 20 + }, + "template": "" + }, + { + "type": "zoom", + "order": 6, + "options": { + "display": true, + "x": 20, + "y": 20 + }, + "template": "+ -" + }, + { + "type": "loader", + "order": 8, + "options": { + "display": true, + "x": 20, + "y": 150 + }, + "template": "
" + }, + { + "type": "logo", + "order": 9, + "options": { + "display": true, + "x": 10, + "y": 40 + }, + "template": "" + } + ], + "title": "002-Test", + "updated_at": "2017-10-24T12:05:16+00:00", + "user": { + "fullname": "cartojs-test", + "avatar_url": "//cartodb-libs.global.ssl.fastly.net/cartodbui/assets/unversioned/images/avatars/avatar_planet_orange.png", + "profile_url": "https://cartojs-test.carto.com" + }, + "version": "3.0.0", + "widgets": [], + "zoom": 7, + "analyses": [ + { + "id": "c0", + "type": "source", + "params": { + "query": "SELECT * FROM ne_10m_populated_places_simple" + }, + "options": { + "table_name": "ne_10m_populated_places_simple", + "simple_geom": "point" + } + }, + { + "id": "b0", + "type": "source", + "params": { + "query": "SELECT * FROM ne_10m_populated_places_simple" + }, + "options": { + "table_name": "ne_10m_populated_places_simple", + "simple_geom": "point" + } + } + ] +} diff --git a/examples/internal/viz/003.json b/examples/internal/viz/003.json new file mode 100644 index 0000000..79db9cc --- /dev/null +++ b/examples/internal/viz/003.json @@ -0,0 +1,53 @@ +{ + "id": "2b13c956-e7c1-11e2-806b-5404a6a683d5", + "version": "0.1.0", + "title": "european_countries_e 0", + "likes": 0, + "description": null, + "scrollwheel": true, + "legends": true, + "url": null, + "map_provider": "leaflet", + "center": "[52.5897007687178, 52.734375]", + "zoom": 4, + "updated_at": "2015-03-13T11:24:37+00:00", + "datasource": { + "user_name": "documentation", + "maps_api_template": "http://{user}.cartodb.com:80", + "force_cors": true, + "stat_tag": "84ec6844-4b4b-11e5-9c1d-080027880ca6" + }, + "layers": [ + { + "options": { + "id": "0a3d9104-99c6-482b-9f8c-7c6134bddcdc", + "order": 0, + "visible": true, + "type": "Tiled", + "name": "Positron", + "className": "default positron_rainbow", + "baseType": "positron_rainbow", + "urlTemplate": "http://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png", + "read_only": true, + "minZoom": "0", + "maxZoom": "18", + "attribution": "© OpenStreetMap contributors", + "subdomains": "abcd", + "category": "CartoDB" + }, + "infowindow": null, + "tooltip": null, + "id": "0a3d9104-99c6-482b-9f8c-7c6134bddcdc", + "order": 0, + "type": "tiled" + } + ], + "overlays": [ + { + "type": "search", + "options": { + "display": true + } + } + ] +} diff --git a/examples/internal/vizjson.html b/examples/internal/vizjson.html new file mode 100644 index 0000000..21be590 --- /dev/null +++ b/examples/internal/vizjson.html @@ -0,0 +1,89 @@ + + + + + + + + Base example | CARTO + + + + + + + + + + + + + + + + + +
+ + + diff --git a/examples/public/dataviews/category.html b/examples/public/dataviews/category.html new file mode 100644 index 0000000..b435e5f --- /dev/null +++ b/examples/public/dataviews/category.html @@ -0,0 +1,99 @@ + + + + Category widget | CARTO + + + + + + + + +
+
    +
  • +

    Column

    + +
  • +
  • +

    Limit

    + +
  • +
  • +

    Operation

    + +
  • +
  • +

    Operation column

    + +
  • +
+ +

+    
+ + + + + diff --git a/examples/public/dataviews/error-handling.html b/examples/public/dataviews/error-handling.html new file mode 100644 index 0000000..ebba3b7 --- /dev/null +++ b/examples/public/dataviews/error-handling.html @@ -0,0 +1,107 @@ + + + + Widgets error handling | CARTO + + + + + + + + +
+
    +
  • +

    Column

    + +
  • +
  • +

    Operation

    + +
  • +
+ + +

+    
+ + + + + diff --git a/examples/public/dataviews/formula.html b/examples/public/dataviews/formula.html new file mode 100644 index 0000000..88345db --- /dev/null +++ b/examples/public/dataviews/formula.html @@ -0,0 +1,86 @@ + + + + Formula widget | CARTO + + + + + + + + +
+
    +
  • +

    Column

    + +
  • +
  • +

    Operation

    + +
  • +
+ +

+    
+ + + + + diff --git a/examples/public/dataviews/histogram.html b/examples/public/dataviews/histogram.html new file mode 100644 index 0000000..3ea15c9 --- /dev/null +++ b/examples/public/dataviews/histogram.html @@ -0,0 +1,99 @@ + + + + Histogram widget | CARTO + + + + + + + + +
+
    +
  • +

    Column

    + +
  • +
  • +

    Bins

    + +
  • +
  • +

    Start

    + +
  • +
  • +

    End

    + +
  • +
+ +

+    
+ + + + + diff --git a/examples/public/dataviews/time-series.html b/examples/public/dataviews/time-series.html new file mode 100644 index 0000000..f39b60a --- /dev/null +++ b/examples/public/dataviews/time-series.html @@ -0,0 +1,98 @@ + + + + Time Series widget | CARTO + + + + + + + + +
+
    +
  • +

    Column

    + +
  • +
  • +

    Aggregation

    + +
  • +
  • +

    Offset

    + +
  • +
+ +

+    
+ + + + + diff --git a/examples/public/filters/and-filter.html b/examples/public/filters/and-filter.html new file mode 100644 index 0000000..3381a65 --- /dev/null +++ b/examples/public/filters/and-filter.html @@ -0,0 +1,153 @@ + + + + AND Filter | CARTO + + + + + + + + + + + +
+ + + + + diff --git a/examples/public/filters/bounding-box-gmaps.html b/examples/public/filters/bounding-box-gmaps.html new file mode 100644 index 0000000..6f76f5d --- /dev/null +++ b/examples/public/filters/bounding-box-gmaps.html @@ -0,0 +1,115 @@ + + + + + Bounding Box filter | CARTO + + + + + + + + + + + +
+ + + + + diff --git a/examples/public/filters/bounding-box-leaflet.html b/examples/public/filters/bounding-box-leaflet.html new file mode 100644 index 0000000..bcc9ff9 --- /dev/null +++ b/examples/public/filters/bounding-box-leaflet.html @@ -0,0 +1,104 @@ + + + + Bounding Box filter | CARTO + + + + + + + + + + + +
+ + + + + diff --git a/examples/public/filters/category-filter.html b/examples/public/filters/category-filter.html new file mode 100644 index 0000000..a4dbba0 --- /dev/null +++ b/examples/public/filters/category-filter.html @@ -0,0 +1,113 @@ + + + + Category Filter | CARTO + + + + + + + + + + + +
+ + + + + diff --git a/examples/public/filters/circle-filter.html b/examples/public/filters/circle-filter.html new file mode 100644 index 0000000..9230128 --- /dev/null +++ b/examples/public/filters/circle-filter.html @@ -0,0 +1,334 @@ + + + + + Filter data on map with Circle | CARTO + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + diff --git a/examples/public/filters/complex-filter.html b/examples/public/filters/complex-filter.html new file mode 100644 index 0000000..3f04c9e --- /dev/null +++ b/examples/public/filters/complex-filter.html @@ -0,0 +1,90 @@ + + + + Complex Filter Example | CARTO + + + + + + + + + + + +
+ + + + + diff --git a/examples/public/filters/custom-bounding-box-leaflet.html b/examples/public/filters/custom-bounding-box-leaflet.html new file mode 100644 index 0000000..29b875e --- /dev/null +++ b/examples/public/filters/custom-bounding-box-leaflet.html @@ -0,0 +1,139 @@ + + + + Custom Bounding Box filter | CARTO + + + + + + + + + + + + + +
+ + + + + diff --git a/examples/public/filters/polygon-filter.html b/examples/public/filters/polygon-filter.html new file mode 100644 index 0000000..a8a213c --- /dev/null +++ b/examples/public/filters/polygon-filter.html @@ -0,0 +1,332 @@ + + + + + Filter data on map with Polygon | CARTO + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + diff --git a/examples/public/filters/range-filter.html b/examples/public/filters/range-filter.html new file mode 100644 index 0000000..28dcb09 --- /dev/null +++ b/examples/public/filters/range-filter.html @@ -0,0 +1,105 @@ + + + + Range Filter | CARTO + + + + + + + + + + + +
+ + + + + diff --git a/examples/public/layers/change-feature-columns-gmaps.html b/examples/public/layers/change-feature-columns-gmaps.html new file mode 100644 index 0000000..2486565 --- /dev/null +++ b/examples/public/layers/change-feature-columns-gmaps.html @@ -0,0 +1,117 @@ + + + + + Change feature columns | CARTO + + + + + + + + + + + +
+
+ + + + + + + diff --git a/examples/public/layers/change-feature-columns-leaflet.html b/examples/public/layers/change-feature-columns-leaflet.html new file mode 100644 index 0000000..75f4234 --- /dev/null +++ b/examples/public/layers/change-feature-columns-leaflet.html @@ -0,0 +1,107 @@ + + + + Change feature columns | CARTO + + + + + + + + + + + +
+
+ + + + + + diff --git a/examples/public/layers/change-order-gmaps.html b/examples/public/layers/change-order-gmaps.html new file mode 100644 index 0000000..1fc8548 --- /dev/null +++ b/examples/public/layers/change-order-gmaps.html @@ -0,0 +1,113 @@ + + + + + Change order | CARTO + + + + + + + + + + + +
+ + + + + diff --git a/examples/public/layers/change-order-leaflet.html b/examples/public/layers/change-order-leaflet.html new file mode 100644 index 0000000..813a3ab --- /dev/null +++ b/examples/public/layers/change-order-leaflet.html @@ -0,0 +1,103 @@ + + + + Change order | CARTO + + + + + + + + + + + +
+ + + + diff --git a/examples/public/layers/change-source-gmaps.html b/examples/public/layers/change-source-gmaps.html new file mode 100644 index 0000000..57d9c2f --- /dev/null +++ b/examples/public/layers/change-source-gmaps.html @@ -0,0 +1,114 @@ + + + + + Change source | CARTO + + + + + + + + + + + +
+ + + + + + diff --git a/examples/public/layers/change-source-leaflet.html b/examples/public/layers/change-source-leaflet.html new file mode 100644 index 0000000..573d86c --- /dev/null +++ b/examples/public/layers/change-source-leaflet.html @@ -0,0 +1,100 @@ + + + + Change source | CARTO + + + + + + + + + + + +
+ + + + + diff --git a/examples/public/layers/change-style-gmaps.html b/examples/public/layers/change-style-gmaps.html new file mode 100644 index 0000000..f31089a --- /dev/null +++ b/examples/public/layers/change-style-gmaps.html @@ -0,0 +1,117 @@ + + + + + Change style | CARTO + + + + + + + + + + + +
+ + + + diff --git a/examples/public/layers/change-style-leaflet.html b/examples/public/layers/change-style-leaflet.html new file mode 100644 index 0000000..66b3566 --- /dev/null +++ b/examples/public/layers/change-style-leaflet.html @@ -0,0 +1,107 @@ + + + + Change style | CARTO + + + + + + + + + + + +
+ + + + diff --git a/examples/public/layers/feature-click-gmaps.html b/examples/public/layers/feature-click-gmaps.html new file mode 100644 index 0000000..2d16fbd --- /dev/null +++ b/examples/public/layers/feature-click-gmaps.html @@ -0,0 +1,89 @@ + + + + + Feature click | CARTO + + + + + + + + + + + +
+
+ + + + + + + diff --git a/examples/public/layers/feature-click-leaflet.html b/examples/public/layers/feature-click-leaflet.html new file mode 100644 index 0000000..d60aa07 --- /dev/null +++ b/examples/public/layers/feature-click-leaflet.html @@ -0,0 +1,77 @@ + + + + Feature click | CARTO + + + + + + + + + + + +
+ + + + + + diff --git a/examples/public/layers/feature-over-out-gmaps.html b/examples/public/layers/feature-over-out-gmaps.html new file mode 100644 index 0000000..347e80a --- /dev/null +++ b/examples/public/layers/feature-over-out-gmaps.html @@ -0,0 +1,111 @@ + + + + Feature over/out | CARTO + + + + + + + + + + + +
+
+ + + + + + diff --git a/examples/public/layers/feature-over-out-leaflet.html b/examples/public/layers/feature-over-out-leaflet.html new file mode 100644 index 0000000..4b27ee7 --- /dev/null +++ b/examples/public/layers/feature-over-out-leaflet.html @@ -0,0 +1,102 @@ + + + + Feature over/out | CARTO + + + + + + + + + + + +
+
+ + + + + + diff --git a/examples/public/layers/layer-with-aggregation-cluster-gmaps.html b/examples/public/layers/layer-with-aggregation-cluster-gmaps.html new file mode 100644 index 0000000..52e0a0b --- /dev/null +++ b/examples/public/layers/layer-with-aggregation-cluster-gmaps.html @@ -0,0 +1,98 @@ + + + + Layer with aggregation cluster | CARTO + + + + + + + + + + +
+
+ + + + + + diff --git a/examples/public/layers/layer-with-aggregation-cluster-leaflet.html b/examples/public/layers/layer-with-aggregation-cluster-leaflet.html new file mode 100644 index 0000000..3d18cb1 --- /dev/null +++ b/examples/public/layers/layer-with-aggregation-cluster-leaflet.html @@ -0,0 +1,90 @@ + + + + Layer with aggregation cluster | CARTO + + + + + + + + + + + +
+
+ + + + + + diff --git a/examples/public/layers/layer-with-aggregation-gmaps.html b/examples/public/layers/layer-with-aggregation-gmaps.html new file mode 100644 index 0000000..89e76f0 --- /dev/null +++ b/examples/public/layers/layer-with-aggregation-gmaps.html @@ -0,0 +1,82 @@ + + + + Layer with aggregation | CARTO + + + + + + + + + + + +
+
+ + + + + + diff --git a/examples/public/layers/layer-with-aggregation-leaflet.html b/examples/public/layers/layer-with-aggregation-leaflet.html new file mode 100644 index 0000000..3c13baa --- /dev/null +++ b/examples/public/layers/layer-with-aggregation-leaflet.html @@ -0,0 +1,73 @@ + + + + Layer with aggregation | CARTO + + + + + + + + + + + +
+
+ + + + + + diff --git a/examples/public/layers/layer-with-zoom-options-gmaps.html b/examples/public/layers/layer-with-zoom-options-gmaps.html new file mode 100644 index 0000000..922845d --- /dev/null +++ b/examples/public/layers/layer-with-zoom-options-gmaps.html @@ -0,0 +1,51 @@ + + + + Layer with zoom options | CARTO + + + + + + + + + + + +
+ + + diff --git a/examples/public/layers/layer-with-zoom-options-leaflet.html b/examples/public/layers/layer-with-zoom-options-leaflet.html new file mode 100644 index 0000000..5e87076 --- /dev/null +++ b/examples/public/layers/layer-with-zoom-options-leaflet.html @@ -0,0 +1,43 @@ + + + + Layer with zoom options | CARTO + + + + + + + + + + + +
+ + + diff --git a/examples/public/layers/multilayer-gmaps.html b/examples/public/layers/multilayer-gmaps.html new file mode 100644 index 0000000..ddfcc9d --- /dev/null +++ b/examples/public/layers/multilayer-gmaps.html @@ -0,0 +1,85 @@ + + + + Multilayer | CARTO + + + + + + + + + + + +
+ + + + + + diff --git a/examples/public/layers/multilayer-leaflet.html b/examples/public/layers/multilayer-leaflet.html new file mode 100644 index 0000000..1e237d9 --- /dev/null +++ b/examples/public/layers/multilayer-leaflet.html @@ -0,0 +1,76 @@ + + + + Multilayer | CARTO + + + + + + + + + + + +
+ + + + + + diff --git a/examples/public/layers/single-layer-gmaps.html b/examples/public/layers/single-layer-gmaps.html new file mode 100644 index 0000000..f48cec9 --- /dev/null +++ b/examples/public/layers/single-layer-gmaps.html @@ -0,0 +1,67 @@ + + + + Single layer | CARTO + + + + + + + + + + + +
+ + + + + + diff --git a/examples/public/layers/single-layer-leaflet.html b/examples/public/layers/single-layer-leaflet.html new file mode 100644 index 0000000..0ec60dc --- /dev/null +++ b/examples/public/layers/single-layer-leaflet.html @@ -0,0 +1,61 @@ + + + + Single layer | CARTO + + + + + + + + + + + + + +
+
+ + + + + + diff --git a/examples/public/misc/boilerplate-gmaps.html b/examples/public/misc/boilerplate-gmaps.html new file mode 100644 index 0000000..9bee575 --- /dev/null +++ b/examples/public/misc/boilerplate-gmaps.html @@ -0,0 +1,15 @@ + + + + App | CARTO + + + + + + + + + + + diff --git a/examples/public/misc/boilerplate-leaflet.html b/examples/public/misc/boilerplate-leaflet.html new file mode 100644 index 0000000..0e205d3 --- /dev/null +++ b/examples/public/misc/boilerplate-leaflet.html @@ -0,0 +1,16 @@ + + + + App | CARTO + + + + + + + + + + + + diff --git a/examples/public/misc/custom-search-dataset-gmaps.html b/examples/public/misc/custom-search-dataset-gmaps.html new file mode 100644 index 0000000..e6bd100 --- /dev/null +++ b/examples/public/misc/custom-search-dataset-gmaps.html @@ -0,0 +1,178 @@ + + + + Custom search dataset | CARTO + + + + + + + + + + + +
+ + + + + + diff --git a/examples/public/misc/custom-search-dataset-leaflet.html b/examples/public/misc/custom-search-dataset-leaflet.html new file mode 100644 index 0000000..f3df32b --- /dev/null +++ b/examples/public/misc/custom-search-dataset-leaflet.html @@ -0,0 +1,147 @@ + + + + Custom search dataset | CARTO + + + + + + + + + + + + +
+ + + + + + diff --git a/examples/public/misc/edit-sql-cartocss-gmaps.html b/examples/public/misc/edit-sql-cartocss-gmaps.html new file mode 100644 index 0000000..1509527 --- /dev/null +++ b/examples/public/misc/edit-sql-cartocss-gmaps.html @@ -0,0 +1,119 @@ + + + + Edit SQL & CartoCSS | CARTO + + + + + + + + + + +
+ + + + + diff --git a/examples/public/misc/edit-sql-cartocss-leaflet.html b/examples/public/misc/edit-sql-cartocss-leaflet.html new file mode 100644 index 0000000..b53f9da --- /dev/null +++ b/examples/public/misc/edit-sql-cartocss-leaflet.html @@ -0,0 +1,111 @@ + + + + Edit SQL CARTOCSS | CARTO + + + + + + + + + + + + +
+ + + + + diff --git a/examples/public/misc/error-handling-gmaps.html b/examples/public/misc/error-handling-gmaps.html new file mode 100644 index 0000000..80d2bb0 --- /dev/null +++ b/examples/public/misc/error-handling-gmaps.html @@ -0,0 +1,201 @@ + + + + Error handling | CARTO + + + + + + + + + + + +
+ + + + + diff --git a/examples/public/misc/error-handling-leaflet.html b/examples/public/misc/error-handling-leaflet.html new file mode 100644 index 0000000..5ad3daa --- /dev/null +++ b/examples/public/misc/error-handling-leaflet.html @@ -0,0 +1,190 @@ + + + + Error handling | CARTO + + + + + + + + + + + + + +
+ + + + + diff --git a/examples/public/misc/filter-data-map-gmaps.html b/examples/public/misc/filter-data-map-gmaps.html new file mode 100644 index 0000000..5a6cc42 --- /dev/null +++ b/examples/public/misc/filter-data-map-gmaps.html @@ -0,0 +1,118 @@ + + + + Filter data on map | CARTO + + + + + + + + + + +
+ + + + + + diff --git a/examples/public/misc/filter-data-map-leaflet.html b/examples/public/misc/filter-data-map-leaflet.html new file mode 100644 index 0000000..0564ab8 --- /dev/null +++ b/examples/public/misc/filter-data-map-leaflet.html @@ -0,0 +1,125 @@ + + + + + Filter data on map | CARTO + + + + + + + + + + + + + + + + + +
+ + + + + + + + diff --git a/examples/public/misc/guide-gmaps.html b/examples/public/misc/guide-gmaps.html new file mode 100644 index 0000000..d78685a --- /dev/null +++ b/examples/public/misc/guide-gmaps.html @@ -0,0 +1,218 @@ + + + + Guide | CARTO + + + + + + + + + + + +
+ + + + + diff --git a/examples/public/misc/guide-leaflet.html b/examples/public/misc/guide-leaflet.html new file mode 100644 index 0000000..22e866a --- /dev/null +++ b/examples/public/misc/guide-leaflet.html @@ -0,0 +1,217 @@ + + + + Guide | CARTO + + + + + + + + + + + +
+ + + + + diff --git a/examples/public/misc/hexagon-aggregation-gmaps.html b/examples/public/misc/hexagon-aggregation-gmaps.html new file mode 100644 index 0000000..0e29f54 --- /dev/null +++ b/examples/public/misc/hexagon-aggregation-gmaps.html @@ -0,0 +1,133 @@ + + + + Hexagon aggregation | CARTO + + + + + + + + + + +
+ + + + + + diff --git a/examples/public/misc/hexagon-aggregation-leaflet.html b/examples/public/misc/hexagon-aggregation-leaflet.html new file mode 100644 index 0000000..3d73fd4 --- /dev/null +++ b/examples/public/misc/hexagon-aggregation-leaflet.html @@ -0,0 +1,127 @@ + + + + Hexagon aggregation | CARTO + + + + + + + + + + + +
+ + + + + diff --git a/examples/public/misc/legends-gmaps.html b/examples/public/misc/legends-gmaps.html new file mode 100644 index 0000000..27ed7ee --- /dev/null +++ b/examples/public/misc/legends-gmaps.html @@ -0,0 +1,193 @@ + + + + Legends | CARTO + + + + + + + + + + +
+ + + + + diff --git a/examples/public/misc/legends-leaflet.html b/examples/public/misc/legends-leaflet.html new file mode 100644 index 0000000..2ff2a66 --- /dev/null +++ b/examples/public/misc/legends-leaflet.html @@ -0,0 +1,185 @@ + + + + Legends | CARTO + + + + + + + + + + + +
+ + + + + diff --git a/examples/public/misc/piechart-vega.html b/examples/public/misc/piechart-vega.html new file mode 100644 index 0000000..e921479 --- /dev/null +++ b/examples/public/misc/piechart-vega.html @@ -0,0 +1,219 @@ + + + + CARTO.js + Category Dataviews + Vega Pie Chart + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + diff --git a/examples/public/misc/populated-places-gmaps.html b/examples/public/misc/populated-places-gmaps.html new file mode 100644 index 0000000..7ba9741 --- /dev/null +++ b/examples/public/misc/populated-places-gmaps.html @@ -0,0 +1,140 @@ + + + + Populated places | CARTO + + + + + + + + + + +
+ + + + + diff --git a/examples/public/misc/populated-places-leaflet.html b/examples/public/misc/populated-places-leaflet.html new file mode 100644 index 0000000..d8eb9af --- /dev/null +++ b/examples/public/misc/populated-places-leaflet.html @@ -0,0 +1,132 @@ + + + + Populated places | CARTO + + + + + + + + + + + +
+ + + + + diff --git a/examples/public/misc/popups-gmaps.html b/examples/public/misc/popups-gmaps.html new file mode 100644 index 0000000..c23ad47 --- /dev/null +++ b/examples/public/misc/popups-gmaps.html @@ -0,0 +1,134 @@ + + + + Pop-ups | CARTO + + + + + + + + + + + +
+ + + + + diff --git a/examples/public/misc/popups-leaflet.html b/examples/public/misc/popups-leaflet.html new file mode 100644 index 0000000..754cbdf --- /dev/null +++ b/examples/public/misc/popups-leaflet.html @@ -0,0 +1,129 @@ + + + + Pop-ups | CARTO + + + + + + + + + + + + +
+ + + + + diff --git a/examples/public/misc/popups-video-gmaps.html b/examples/public/misc/popups-video-gmaps.html new file mode 100644 index 0000000..7c8651b --- /dev/null +++ b/examples/public/misc/popups-video-gmaps.html @@ -0,0 +1,97 @@ + + + + Pop-ups with embed video | CARTO + + + + + + + + + + + +
+ + + + + diff --git a/examples/public/misc/popups-video-leaflet.html b/examples/public/misc/popups-video-leaflet.html new file mode 100644 index 0000000..92bdc26 --- /dev/null +++ b/examples/public/misc/popups-video-leaflet.html @@ -0,0 +1,92 @@ + + + + Pop-ups with embed video | CARTO + + + + + + + + + + + + +
+ + + + + diff --git a/examples/public/misc/storymap-leaflet.html b/examples/public/misc/storymap-leaflet.html new file mode 100644 index 0000000..f31fb42 --- /dev/null +++ b/examples/public/misc/storymap-leaflet.html @@ -0,0 +1,194 @@ + + + + Storytelling map using Storymap.js | CARTO + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+

Spain, France and Portugal

+
+
+

Spain

+

+ Source Wikipedia +

+ +

+ Spain (Spanish: España [esˈpaɲa] (About this sound listen)), officially the Kingdom of Spain (Spanish: Reino de España),[a][b] + is a sovereign state located on the Iberian Peninsula in southwestern Europe, with two large archipelagoes, + the Balearic Islands in the Mediterranean Sea and the Canary Islands off the North African Atlantic + coast, two cities, Ceuta and Melilla, in the North African mainland and several small islands in + the Alboran Sea near the Moroccan coast. The country's mainland is bordered to the south and east + by the Mediterranean Sea except for a small land boundary with Gibraltar; to the north and northeast + by France, Andorra, and the Bay of Biscay; and to the west and northwest by Portugal and the Atlantic + Ocean. It is the only European country to have a border with an African country (Morocco)[h] and + its African territory accounts for nearly 5% of its population, mostly in the Canary Islands but + also in Ceuta and Melilla. +

+
+
+

France

+

+ Source Wikipedia +

+

+ France (French IPA: [fʁɑ̃s]), officially the French Republic (French: République française [ʁepyblik fʁɑ̃sɛz]), is a country + whose territory consists of metropolitan France in western Europe, as well as several overseas regions + and territories.[XIII] The metropolitan area of France extends from the Mediterranean Sea to the + English Channel and the North Sea, and from the Rhine to the Atlantic Ocean. The overseas territories + include French Guiana in South America and several islands in the Atlantic, Pacific and Indian oceans. + The country's 18 integral regions (5 of which are situated overseas) span a combined area of 643,801 + square kilometres (248,573 sq mi) which, as of October 2017, has a population of 67.15 million people.[10] + France is a unitary semi-presidential republic with its capital in Paris, the country's largest city + and main cultural and commercial centre. Other major urban centres include Marseille, Lyon, Lille, + Nice, Toulouse and Bordeaux. +

+
+
+

Portugal

+

+ Source Wikipedia +

+

+ Portugal (Portuguese: [puɾtuˈɣaɫ]), officially the Portuguese Republic (Portuguese: República Portuguesa [ʁɛ'puβlikɐ puɾtu'ɣezɐ]),[note + 1] is a sovereign state located on the Iberian Peninsula in southwestern Europe. It is the westernmost + country of mainland Europe, being bordered to the west and south by the Atlantic Ocean and to the + north and east by Spain. Its territory also includes the Atlantic archipelagos of the Azores and + Madeira, both autonomous regions with their own regional governments. At 1.7 million km2, its Exclusive + Economic Zone is the 3rd largest in the European Union and the 11th largest in the world. +

+
+
+
+
+ + + + + + diff --git a/examples/public/style.css b/examples/public/style.css new file mode 100644 index 0000000..28b653f --- /dev/null +++ b/examples/public/style.css @@ -0,0 +1,487 @@ +* { + box-sizing: border-box; +} + +body { + margin: 0; + padding: 0; +} + +#map { + position: absolute; + height: 100%; + width: 100%; + z-index: 0; +} + +aside.toolbox { + position: absolute; + top: 24px; + right: 24px; + min-width: 300px; + max-width: 300px; + z-index: 2; +} + +/* UTILITIES */ + +.mt-16 { + margin-top: 16px !important; +} +.p-8 { + padding: 8px !important; +} +.bg-white { + background: #FFFFFF; +} +.bg-red { + background: #EE4D5A; +} +.bg-orange { + background: #EF9E4E; +} +.bg-gray { + background: #F9F9F9; +} +.text-white { + color: #FFFFFF !important; +} +.text-red { + color: #F15743; +} +.h2 { + color: #2E3C43; + line-height: 32px; + font-family: 'Montserrat', Arial, Helvetica, sans-serif; + font-size: 20px; + font-weight: 600; +} +.open-sans { + color: #747D82; + font-size: 12px; + line-height: 16px; + font-family: 'Open Sans', Arial, Helvetica, sans-serif; + font-weight: 400; +} +.button { + border: 1px solid #1785FB; + color: #1785FB; + border-radius: 4px; + cursor: pointer; + text-transform: uppercase; + font-weight: 600; + outline: none; + background: transparent; +} +.button-error { + border: 1px solid #F15743; + color: #F15743; + border-radius: 4px; + cursor: pointer; + text-transform: uppercase; + font-weight: 600; + outline: none; + background: transparent; +} + +/* STYLES */ + +.github-logo { + position: absolute; + top: 20px; + right: 16px; + width: 24px; + height: 24px; + border: 0; + padding: 0; + background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHdpZHRoPSIyNHB4IiBoZWlnaHQ9IjI0cHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIj4gICAgICAgIDx0aXRsZT5TaGFwZTwvdGl0bGU+ICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPiAgICA8ZGVmcz48L2RlZnM+ICAgIDxnIGlkPSJEb2NzIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj4gICAgICAgIDxnIGlkPSJEb2N1bWVudGF0aW9uLS0tQVBJLS0tRXhhbXBsZXMiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMzc2LjAwMDAwMCwgLTExNi4wMDAwMDApIiBmaWxsPSIjQkFCRUMwIiBmaWxsLXJ1bGU9Im5vbnplcm8iPiAgICAgICAgICAgIDxnIGlkPSJGbG9hdC0yIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxMTE1LjAwMDAwMCwgOTYuMDAwMDAwKSI+ICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik0yNzIuOTk4OTU3LDIwIEMyNjYuMzcyODcsMjAgMjYxLDI1LjUwNzIyMjIgMjYxLDMyLjMwMjQ1ODMgQzI2MSwzNy43MzkwNzUxIDI2NC40MzcyMTcsNDIuMzQ5ODIyNSAyNjkuMjA2OTU3LDQzLjk3ODAyNjIgQzI2OS44MDU5MTMsNDQuMDg5MjgzMiAyNjkuOTk1ODI2LDQzLjcyNTU1ODMgMjY5Ljk5NTgyNiw0My4zOTkyNzU3IEMyNjkuOTk1ODI2LDQzLjEwNjE1NjMgMjcwLjAwMjA4Nyw0Mi4yODk5MTQ5IDI2OS45OTU4MjYsNDEuMjYyOTI3MSBDMjY2LjY1ODc4Myw0Mi4wMDUzNTM3IDI2NS45NjY5NTcsMzkuNjQyMjExOSAyNjUuOTY2OTU3LDM5LjY0MjIxMTkgQzI2NS40MjIyNjEsMzguMjIwNDc1NyAyNjQuNjM2NTIyLDM3Ljg0Mjg0MzcgMjY0LjYzNjUyMiwzNy44NDI4NDM3IEMyNjMuNTQ2MDg3LDM3LjA3OTAyMTUgMjY0LjcxNzkxMywzNy4wOTUwNjgyIDI2NC43MTc5MTMsMzcuMDk1MDY4MiBDMjY1LjkyMTA0MywzNy4xODA2NTA1IDI2Ni41NTY1MjIsMzguMzYxNjg2NSAyNjYuNTU2NTIyLDM4LjM2MTY4NjUgQzI2Ny42MjcxMyw0MC4yNDIzNTc5IDI2OS4zNjQ1MjIsMzkuNjk4OTEwMiAyNzAuMDQ4LDM5LjM4NDM5NTIgQzI3MC4xNTc1NjUsMzguNTkwNjE5MiAyNzAuNDY3NDc4LDM4LjA0NjEwMTcgMjcwLjgxMDc4MywzNy43NDEyMTQ3IEMyNjguMTQ1NzM5LDM3LjQyODgzOTIgMjY1LjM0NTA0MywzNi4zNzUxMDY5IDI2NS4zNDUwNDMsMzEuNjU4NDUxNCBDMjY1LjM0NTA0MywzMC4zMTU4Nzg3IDI2NS44MTI1MjIsMjkuMjE4Mjg1NSAyNjYuNTgwNTIyLDI4LjM1NzExMzQgQzI2Ni40NTYzNDgsMjguMDQ3OTQ3MyAyNjYuMDQzMTMsMjYuNzk2MzA1OSAyNjYuNjk2MzQ4LDI1LjEwMjg0NTggQzI2Ni42OTYzNDgsMjUuMTAyODQ1OCAyNjcuNzA1MzkxLDI0Ljc3MTIxNDMgMjY5Ljk5NTgyNiwyNi4zNjUxODUgQzI3MC45NTM3MzksMjYuMDkxMzIxNSAyNzEuOTgwNTIyLDI1Ljk1NTQ1OTYgMjczLjAwMTA0MywyNS45NTAxMTA3IEMyNzQuMDIwNTIyLDI1Ljk1NTQ1OTYgMjc1LjA0NjI2MSwyNi4wOTEzMjE1IDI3Ni4wMDYyNjEsMjYuMzY1MTg1IEMyNzguMjk2Njk2LDI0Ljc3MzM1MzggMjc5LjMwMjYwOSwyNS4xMDI4NDU4IDI3OS4zMDI2MDksMjUuMTAyODQ1OCBDMjc5Ljk1Njg3LDI2Ljc5NjMwNTkgMjc5LjU0NDY5NiwyOC4wNDkwMTcxIDI3OS40MjE1NjUsMjguMzU4MTgzMiBDMjgwLjE4OTU2NSwyOS4yMTkzNTUzIDI4MC42NTYsMzAuMzE2OTQ4NSAyODAuNjU2LDMxLjY1OTUyMTIgQzI4MC42NTYsMzYuMzg3OTQ0MyAyNzcuODUxMTMsMzcuNDI2Njk5NyAyNzUuMTc2Njk2LDM3LjcyOTQ0NzEgQzI3NS42MDY2MDksMzguMTExMzU4MiAyNzUuOTk3OTEzLDM4Ljg0OTUwNTcgMjc1Ljk5NzkxMywzOS45OTczNzg2IEMyNzUuOTk3OTEzLDQxLjUzNTcyMDcgMjc1Ljk5NzkxMyw0Mi45OTkxNzg0IDI3NS45OTc5MTMsNDMuNDAzNTU0OCBDMjc1Ljk5NzkxMyw0My43MzMwNDY4IDI3Ni4xOTIsNDQuMDk1NzAxOSAyNzYuODAxMzkxLDQzLjk3Njk1NjQgQzI4MS41NjU5MTMsNDIuMzQ2NjEzMiAyODUsMzcuNzM2OTM1NiAyODUsMzIuMzAyNDU4MyBDMjg1LDI1LjUwNzIyMjIgMjc5LjYyODE3NCwyMCAyNzIuOTk4OTU3LDIwIFoiIGlkPSJTaGFwZSI+PC9wYXRoPiAgICAgICAgICAgIDwvZz4gICAgICAgIDwvZz4gICAgPC9nPjwvc3ZnPg==); + background-color: transparent; + cursor: pointer; +} + +.box { + background: #FFFFFF; + z-index: 2; + border-radius: 4px; + padding: 16px; + margin: 0 0 24px; + box-shadow: 0 0px 16px rgba(0, 0, 0, 0.24); +} +.box header h1 { + display: inline-block; + color: #2E3C43; + line-height: 32px; + font-family: 'Montserrat', Arial, Helvetica, sans-serif; + font-size: 20px; + font-weight: 600; + margin: 0; + width: calc(100% - 24px); +} +.box section .separator { + min-height: 1px; + background-color: rgba(46, 60, 67, 0.08); + margin: 16px 0; +} +.box section .usage { + position: relative; + background-color: #F9F9F9; + padding: 16px; + border-left: #979797 solid 1px; +} +.box section .usage:after { + content: ''; + position: absolute; + top: 0; + right: 0; + border-width: 0 20px 20px 0; + border-style: solid; + border-color: #979797 #fff; +} +.box section .usage header { + font-family: 'Montserrat', Arial, Helvetica, sans-serif; + font-size: 12px; + font-weight: 600; + margin: 0 0 12px; +} +.box section .usage p{ + margin: 0 0 16px; +} +.box section .usage p:last-child{ + margin: 0; +} +.box section .description { + margin-bottom: 0; +} + +/* BOX CONTROLS */ +#controls ul { + padding: 0; + margin-bottom: 0; +} +#controls li { + list-style-type: none; + margin: 0 0 8px 0; + display: flex; + vertical-align: middle; +} +#controls li input { + margin: 0 8px 0 0; +} +#controls li input[type=checkbox] { + margin: 2px 8px 0 0; +} +#controls li label { + font: 12px/16px 'Open Sans'; + cursor: pointer; +} +#controls li:last-child { + margin-bottom: 0; +} +#controls li:hover { + cursor: pointer; +} +#controls h2 { + margin: 16px 0 8px; +} +#controls #info h3 { + color: #2E3C43; + line-height: 32px; + font-family: 'Montserrat', Arial, Helvetica, sans-serif; + font-size: 20px; + font-weight: 600; + margin: 16px 0 8px; +} +#controls #info p { + margin: 0; +} + +/* WIDGETS */ + +.widget h2, .legend h2 { + margin: 0 0 8px; +} +.widget h3, .legend h3 { + color: #2E3C43; + line-height: 24px; + font-family: 'Montserrat', Arial, Helvetica, sans-serif; + font-size: 16px; + font-weight: 600; + margin: 0 0 8px; +} +.widget p { + margin: 0 0 16px; +} +.widget ul { + padding: 0; +} +.widget li { + list-style-type: none; +} +.widget .category { + color: #2E3C43; + line-height: 16px; + font-family: 'Montserrat', Arial, Helvetica, sans-serif; + font-size: 16px; + font-weight: 600; + margin: 0; +} +.widget textarea { + width: 100%; + resize: none; + padding: 7px 8px 6px; + border: 1px solid #DDD; + border-radius: 4px; + margin-bottom: 8px; +} +.widget button { + padding: 4px 12px; +} + +.widget button:hover { + background: rgba(23,133,251,.08); +} + +/* LEGENDS */ + +.legend ul { + list-style-type: none; + margin-bottom: 24px; + padding: 0; +} +.legend ul:last-child { + margin: 0; + padding: 0; + border: 0; +} +.legend .circle { + width: 8px; + height: 8px; + border-radius: 50%; + margin-right: 8px; +} +.legend .circle-outline { + background: #F9F9F9; + border: 1px solid rgba(0,0,0,0.10); +} +.legend .category { + font: 600; +} +.legend .category li { + margin-bottom: 6px; + display: flex; + align-items: center; + text-transform: uppercase; +} +.legend .category li:last-child { + margin-bottom: 0; +} +.legend .size { + display: flex; + align-items: center; +} +.legend .size > div { + margin-right: 32px; + display: flex; + align-items: center; +} +.legend .size > div:last-child { + margin-right: 0; +} +.legend .size p { + margin: 0; + text-transform: uppercase; +} +.legend .avg{ + margin-top: 12px; +} +.legend .avg p strong { + font-weight: 600; +} + +/* DATAVIEWS */ + +.dataview ul { + display: flex; + list-style-type: none; + flex-wrap: wrap; + width: calc(100% - 376px); + padding: 0; + margin: 0 0 0 40px; +} +.dataview li { + margin: 0 20px 20px 0; +} +.dataview .input_text { + min-height: 32px; + padding: 7px 8px; + border: 1px solid #DDD; + border-radius: 4px; +} +.dataview .input_text:hover, +.dataview .select:hover { + border: 1px solid #1785FB; +} +.dataview .select { + -webkit-appearance: none; + appearance: none; + min-height: 32px; + padding: 7px 8px; + border: 1px solid #DDD; + border-radius: 4px; + background: #fff; + min-width: 150px; +} +.dataview .button { + padding: 8px 20px; + margin: 0 0 0 40px; + cursor: pointer; +} +.code { + margin: 40px; + font-size: 14px; + color: #747D82; +} + +/* TABS */ +.maptabs { + display: flex; + list-style: none; + padding-left: 0; + margin-bottom: 0; +} +.maptabs li.maptab { + flex-grow: 1; + display: flex; + justify-content: center; +} +.maptabs .maptab a { + font: 12px 'Montserrat', 'Helvetica Neue', Helvetica, Arial, + sans-serif; + font-weight: 600; + text-decoration: none; + padding: 12px; + border: 1px solid rgba(22, 39, 69, 0.2); + color: #00B9BF; + transition: all .2s; + flex-grow: 1; + display: flex; + justify-content: center; +} +.maptabs .maptab.is-active a { + background: #2E3C43; + color: #ffffff; + border: 1px solid #2E3C43; + border-right: 0; + box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.15); +} +.maptabs .maptab a:hover { + background: #00B9BF; + border-color: #00B9BF; + color: #ffffff; + box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.15); +} +.maptabs .maptab.is-active a:hover { + background: #2E3C43; + color: #ffffff; + border: 1px solid #2E3C43; +} +.maptabs .maptab:first-child a { + border-radius: 4px 0 0 4px; +} +.maptabs .maptab:last-child a { + border-radius: 0 4px 4px 0; + border-right: 1px solid rgba(22, 39, 69, 0.2); +} + +/* RANGE SLIDER */ + +input[type="range"] { + position: relative; + display: inline-block; + width: 100%; + height: 10px; + background-color: rgb(23, 133, 251); + border-radius: 10px; + outline: none; + cursor: pointer; + margin: 0 0 22px; + -webkit-appearance: none; +} + +input[type=range]::before, +input[type=range]::after { + color: #747D82; + position: absolute; + top: 20px; +} + +input[type=range]::before { + content: attr(min-with-suffix); + left: 0; +} + +input[type=range]::after { + content: attr(max-with-suffix); + right: 0; +} + +input[type="range"]::-webkit-slider-runnable-track, +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; +} + +input[type="range"]::-webkit-slider-thumb { + width: 15px; + height: 15px; + background-color: #fff; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 50%; + box-shadow: 0 5px 15px 0 rgba(20, 75, 102, 0.15); + margin-top: -2px; +} + +input[type="range"]::-moz-range-thumb { + width: 15px; + height: 15px; + background-color: #fff; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 50%; + box-shadow: 0 5px 15px 0 rgba(20, 75, 102, 0.15); + margin-top: -2px; +} + +input[type="range"]::-ms-thumb { + width: 15px; + height: 15px; + background-color: #fff; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 50%; + box-shadow: 0 5px 15px 0 rgba(20, 75, 102, 0.15); + margin-top: -2px; +} + +input[type="range"]::-webkit-slider-runnable-track { + height: 10px; + width: 100%; + background: linear-gradient(to right, transparent calc(var(--value) * 1%), rgb(226, 230, 227) 0); + border-radius: 10px; +} + +input[type="range"]::-moz-range-track { + height: 10px; + background: linear-gradient(to right, transparent calc(var(--value) * 1%), rgb(226, 230, 227) 0); + border-radius: 10px; +} + +input[type="range"]::-ms-track { + background-color: rgb(23, 133, 251); + height: 10px; +} + +input[type="range"]::-ms-fill-lower { + background-color: rgb(226, 230, 227); +} + +input[type="range"]::-ms-fill-upper { + background-color: rgb(23, 133, 251);; +} diff --git a/grunt/README.md b/grunt/README.md new file mode 100644 index 0000000..86b8a17 --- /dev/null +++ b/grunt/README.md @@ -0,0 +1,20 @@ +## Grunt tasks list + +These are the tasks(\*) we provide: + +- [x] ```grunt publish_s3``` => publish CARTO.js library in S3 (you need secret keys). +- [x] ```grunt clean``` => clean temporary and dist folders. +- [x] ```grunt invalidate``` => invalidate library files through fastly. +- [x] ```grunt test``` => run library test suite in the console (it will generate a SpecRunner.html file in test folder). + - ```grunt test -d``` => if you need to print failing tests. + - ```grunt test -f``` => if you need to know where was the problem. +- [x] ```grunt build``` => generate library in dist folder. +- [X] ```grunt build:js``` => create uncompressed javascript files in dist (useful for developing) +- [X] ```grunt build:css``` => create uncompressed stylesheets files in dist (useful for developing) + +Dev tasks are as useful while in development, watches files and updates the bundles accordingly: +- [x] ```grunt dev:js``` => only JS +- [x] ```grunt dev:css``` => only CSS +- [x] ```grunt dev``` => both JS and CSS + +*Remember to install all the things you need, check main README.md, how to build section. diff --git a/grunt/tasks/_browserify-bundles.js b/grunt/tasks/_browserify-bundles.js new file mode 100644 index 0000000..3355b5a --- /dev/null +++ b/grunt/tasks/_browserify-bundles.js @@ -0,0 +1,45 @@ +module.exports = { + 'src-specs': { + src: [ + 'test/fail-tests-if-have-errors-in-src.js', + 'test/spec/api/**/*', + 'test/spec/core/**/*', + 'test/spec/dataviews/**/*', + 'test/spec/util/**/*', + 'test/spec/geo/**/*', + 'test/spec/ui/**/*', + 'test/spec/vis/**/*', + 'test/spec/windshaft/**/*', + 'test/spec/windshaft-integration/**/*', + 'test/spec/analysis/**/*', + 'test/spec/engine.spec.js', + + // not actually used anywhere in cartodb.js, only for editor? + // TODO can be (re)moved? + '!test/spec/ui/common/tabpane.spec.js' + ], + dest: '<%= tmp %>/src-specs.js', + options: { + transform: [ + ['babelify', { + presets: ['env'], + plugins: ['transform-object-rest-spread'] + }] + ], + require: [ 'camshaft-reference/versions/0.59.4/reference.json:./versions/0.59.4/reference.json' ] + } + }, + + cartodb: { + src: [ + 'src/cartodb.js' + ], + exclude: [ + 'src/api/v4/' + ], + dest: '<%= dist %>/internal/cartodb.uncompressed.js', + options: { + require: [ 'camshaft-reference/versions/0.59.4/reference.json:./versions/0.59.4/reference.json' ] + } + } +}; diff --git a/grunt/tasks/browserify.js b/grunt/tasks/browserify.js new file mode 100644 index 0000000..feaee38 --- /dev/null +++ b/grunt/tasks/browserify.js @@ -0,0 +1,34 @@ +var bundles = require('./_browserify-bundles'); + +module.exports = { + task: function () { + var cfg = {}; + for (var name in bundles) { + var bundle = bundles[name]; + cfg[name] = { + src: bundle.src, + dest: bundle.dest, + options: { + watch: '<%= doWatchify %>', + browserifyOptions: { + debug: true // to generate source-maps + } + } + }; + + var opts = bundle.options; + if (opts) { + if (opts.external) cfg[name].options.external = opts.external; + if (opts.require) cfg[name].options.require = opts.require; + if (opts.transform) cfg[name].options.transform = opts.transform; + + var bOpts = bundle.options.browserifyOptions; + if (bOpts) { + if (bOpts.standalone) cfg[name].options.browserifyOptions.standalone = bOpts.standalone; + } + } + } + + return cfg; + } +}; diff --git a/grunt/tasks/clean.js b/grunt/tasks/clean.js new file mode 100644 index 0000000..6d822ee --- /dev/null +++ b/grunt/tasks/clean.js @@ -0,0 +1,21 @@ +/** + * Clean grunt task for CARTO.js + * + */ +module.exports = { + task: function() { + return { + dist_internal: { + files: [{ + dot: true, + src: [ + '.sass-cache', + '.tmp', + '<%= dist %>/internal', + '!<%= dist %>/.git*' + ] + }] + } + } + } +} diff --git a/grunt/tasks/concat.js b/grunt/tasks/concat.js new file mode 100644 index 0000000..ec56d79 --- /dev/null +++ b/grunt/tasks/concat.js @@ -0,0 +1,15 @@ +module.exports = { + task: function() { + return { + themes: { + options: {}, + files: { + // CARTO.js CSSs (themes?) + '<%= dist %>/internal/themes/css/cartodb.css': [ + '.tmp/scss/**/*.css' + ] + } + } + } + } +} diff --git a/grunt/tasks/connect.js b/grunt/tasks/connect.js new file mode 100644 index 0000000..105acac --- /dev/null +++ b/grunt/tasks/connect.js @@ -0,0 +1,21 @@ +/** + * Connect grunt task for CARTO.js website + * + */ +module.exports = { + task: function() { + return { + server: { + options: { + port: 9001, + livereload: 35730, + open: true, + hostname: '0.0.0.0', // to be able to access the server not only from localhost + base: { + path: '.' + } + } + } + } + } +} diff --git a/grunt/tasks/copy.js b/grunt/tasks/copy.js new file mode 100644 index 0000000..92c2d36 --- /dev/null +++ b/grunt/tasks/copy.js @@ -0,0 +1,12 @@ +module.exports = { + task: function () { + return { + fonts: { + expand: true, + cwd: 'node_modules/cartoassets/src/fonts/', + src: [ '**/*.{eot,ttf,woff,svg}' ], + dest: '<%= dist %>/internal/themes/fonts' + } + }; + } +}; diff --git a/grunt/tasks/cssmin.js b/grunt/tasks/cssmin.js new file mode 100644 index 0000000..bae2231 --- /dev/null +++ b/grunt/tasks/cssmin.js @@ -0,0 +1,28 @@ + +/** + * Css lint grunt task for CARTO.js + * + */ + +module.exports = { + task: function() { + return { + dist: { + options: { + check: 'gzip' + } + }, + + themes: { + options: { + banner: '/* CartoDB.css minified version: <%= version %> */', + check: 'gzip' + }, + files: { + '<%= dist %>/internal/themes/css/cartodb.css': ['<%= dist %>/internal/themes/css/cartodb.css'] + } + } + + } + } +} diff --git a/grunt/tasks/exorcise.js b/grunt/tasks/exorcise.js new file mode 100644 index 0000000..38da80e --- /dev/null +++ b/grunt/tasks/exorcise.js @@ -0,0 +1,24 @@ +var bundles = require('./_browserify-bundles'); + +// Extracts inlined source map from files (browserify bundles in this case). +// Expected to be run after browserify task +exports.task = function() { + var files = {}; + + for (var bundleName in bundles) { + if (!/tmp|specs/.test(bundleName)) { + var bundleDest = bundles[bundleName].dest; + var mapDest = bundleDest.replace('.js', '.map'); + files[mapDest] = bundleDest; + } + } + + return { + bundle: { + options: { + strict: true // fail task if sourcemaps are missing + }, + files: files + } + }; +}; diff --git a/grunt/tasks/fastly.js b/grunt/tasks/fastly.js new file mode 100644 index 0000000..2ecca71 --- /dev/null +++ b/grunt/tasks/fastly.js @@ -0,0 +1,21 @@ + +/** + * Fastly invalidation grunt task for CARTO.js + * + */ + +module.exports = { + task: function() { + return { + options: { + key: '<%= secrets.FASTLY_API_KEY %>' + }, + dist: { + options: { + purgeAll: true, + serviceId: '<%= secrets.FASTLY_CARTODB_SERVICE %>' + } + }, + } + } +} diff --git a/grunt/tasks/imagemin.js b/grunt/tasks/imagemin.js new file mode 100644 index 0000000..f45de7e --- /dev/null +++ b/grunt/tasks/imagemin.js @@ -0,0 +1,21 @@ +/** + * Image minifier grunt task for CARTO.js + * + */ +module.exports = { + task: function() { + return { + distImages: { + options: { + progressive: true + }, + files: [{ + expand: true, + cwd: 'themes/img', + src: [ '**/*.{png,jpg,gif,svg}' ], + dest: '<%= dist %>/internal/themes/img' + }] + } + } + } +} diff --git a/grunt/tasks/jasmine.js b/grunt/tasks/jasmine.js new file mode 100644 index 0000000..48838b5 --- /dev/null +++ b/grunt/tasks/jasmine.js @@ -0,0 +1,32 @@ +var _ = require('underscore'); + +var defaultOptions = { + keepRunner: true, + summary: true, + display: 'short', + vendor: [ + // Load & install the source-map-support lib (get proper stack traces from inlined source-maps) + 'node_modules/source-map-support/browser-source-map-support.js', + 'test/install-source-map-support.js', + 'http://maps.googleapis.com/maps/api/js?key=AIzaSyA4KzmztukvT7C49NSlzWkz75Xg3J_UyFI&v=3.32', + 'node_modules/jasmine-ajax/lib/mock-ajax.js', + 'node_modules/leaflet/dist/leaflet-src.js' + ], + helpers: 'test/helpers/SpecHelper.js' +}; + +/** + * Jasmine grunt task for CARTO.js tests + * https://github.com/gruntjs/grunt-contrib-jasmine#options + * Load order: vendor, helpers, source, specs, + */ +module.exports = { + 'cartodb-src': { + src: [], // actual src files are require'd in the *.spec.js files + options: _.defaults({ + outfile: 'test/SpecRunner-src.html', + specs: '<%= tmp %>/src-specs.js', + '--web-security': 'no' + }, defaultOptions) + } +}; diff --git a/grunt/tasks/s3.js b/grunt/tasks/s3.js new file mode 100644 index 0000000..80795ca --- /dev/null +++ b/grunt/tasks/s3.js @@ -0,0 +1,68 @@ +var semver = require('semver'); + +/** + * S3 upload grunt task for CARTO.js + * + */ + +module.exports = { + task: function(version) { + var tasks = { + options: { + accessKeyId: "<%= secrets.AWS_USER_S3_KEY %>", + secretAccessKey: "<%= secrets.AWS_USER_S3_SECRET %>", + bucket: "<%= secrets.AWS_S3_BUCKET %>", + dryRun: false + } + }; + + var major = semver.major(version); + var minor = semver.minor(version); + var patch = semver.patch(version); + var prerelease = semver.prerelease(version); + + if (prerelease) { + /** + * Publish prerelease URLs + */ + var base = 'carto.js/v' + major + '.' + minor + '.' + patch + '-'; + if (prerelease[0]) { // alpha, beta, rc + tasks['js-prerelease'] = createTask(base + prerelease[0]); + } + if (prerelease[1]) { // number + tasks['js-prerelease-number'] = createTask(base + prerelease[0] + '.' + prerelease[1]); + } + } else { + /** + * Publish release URLs + */ + tasks['js-major'] = createTask('carto.js/v' + major); + tasks['js-minor'] = createTask('carto.js/v' + major + '.' + minor); + tasks['js-patch'] = createTask('carto.js/v' + major + '.' + minor + '.' + patch); + } + + function createTask(dest) { + return { + options: { + overwrite: true, + cache: false, + gzip: true, + headers: { + ContentType: 'application/x-javascript' + } + }, + files: [ + { + action: 'upload', + expand: true, + cwd: 'dist/public', + src: '*.js', + dest: dest + } + ] + }; + } + + return tasks; + } +} diff --git a/grunt/tasks/scss.js b/grunt/tasks/scss.js new file mode 100644 index 0000000..b80f5fb --- /dev/null +++ b/grunt/tasks/scss.js @@ -0,0 +1,29 @@ +/** + * SCSS grunt task for CARTO.js + * + */ + +module.exports = { + task: function () { + return { + dist: { + options: { + sourceMap: false, + outputStyle: 'compressed', + includePaths: [ + 'node_modules/cartoassets/src/scss' + ] + }, + files: [{ + expand: true, + src: [ + 'node_modules/cartoassets/src/scss/entry.scss', + 'themes/scss/entry.scss' + ], + dest: '.tmp/scss', + ext: '.css' + }] + } + }; + } +}; diff --git a/grunt/tasks/terser.js b/grunt/tasks/terser.js new file mode 100644 index 0000000..75f0a95 --- /dev/null +++ b/grunt/tasks/terser.js @@ -0,0 +1,29 @@ +var bundles = require('./_browserify-bundles'); + +/** + * Manger-compressor for CARTO.js ES6+ compliant + * + */ +module.exports = { + task: function() { + var cfg = {}; + + for (var bundleName in bundles) { + if (!/tmp|specs/.test(bundleName)) { + var files = {}; + var src = bundles[bundleName].dest; + var uglifiedDest = src.replace('.uncompressed', ''); + files[uglifiedDest] = src; + + cfg[bundleName] = { + options: { + sourceMap: true + }, + files: files + } + } + } + + return cfg; + } +} diff --git a/grunt/tasks/usebanner.js b/grunt/tasks/usebanner.js new file mode 100644 index 0000000..7094480 --- /dev/null +++ b/grunt/tasks/usebanner.js @@ -0,0 +1,24 @@ +/** + * Add banner for CARTO.jss + * + */ +module.exports = { + task: function() { + var cfg = { + options: { + position: 'top', + banner: [ + '// CartoDB.js version: <%= version %>', + '// sha: <%= gitinfo.local.branch.current.SHA %>', + ].join('\n') + }, + files: { + src: [ + '<%= dist %>/internal/cartodb.uncompressed.js', + '<%= dist %>/internal/cartodb.js' + ] + } + }; + return cfg; + } +} diff --git a/grunt/tasks/watch.js b/grunt/tasks/watch.js new file mode 100644 index 0000000..6dadb2a --- /dev/null +++ b/grunt/tasks/watch.js @@ -0,0 +1,27 @@ +/** + * Watch grunt task for CARTO.js + * + */ +module.exports = { + task: function () { + return { + scss: { + files: ['themes/scss/**/*.scss'], + tasks: ['sass', 'concat:themes', 'cssmin:themes'], + options: { + spawn: false, + livereload: 35730 + } + }, + livereload: { + options: { + livereload: '<%= connect.server.options.livereload %>' + }, + files: [ + '.tmp/css/**/*.css', + '<%= dist %>/internal/themes/css/cartodb.css' + ] + } + }; + } +}; diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..4d9564d --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,21 @@ +var gulp = require('gulp'); +var iconfont = require('gulp-iconfont'); +var iconfontCss = require('gulp-iconfont-css'); + +gulp.task('default', function () { + gulp.src(['./themes/svg/*.svg']) + + .pipe(iconfontCss({ + fontName: 'CDBIcon', + path: './themes/svg/_icons.scss', + targetPath: '../scss/icons/icons.scss', + fontPath: '../../fonts/' + })) + + .pipe(iconfont({ + fontName: 'CDBIcon', // required + appendCodepoints: true // recommended option + })) + + .pipe(gulp.dest('themes/fonts/')); +}); diff --git a/index.html b/index.html new file mode 100644 index 0000000..267047b --- /dev/null +++ b/index.html @@ -0,0 +1,50 @@ + + + + + CARTO.js dev server + + + + + + +

+ Tests +

+ + +

Other

+ + + + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..2deb121 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,15982 @@ +{ + "name": "internal-carto.js", + "version": "4.2.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@carto/zera": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@carto/zera/-/zera-1.0.7.tgz", + "integrity": "sha512-I++EJMuybbNohLsaWMpEcs2gSbUhf0JHDgubT6b63kIB8ox5NS9LHeE39vS1DOUQNlDubLp84QDrSIt/aMpA7A==" + }, + "@webassemblyjs/ast": { + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.5.12.tgz", + "integrity": "sha512-bmTBEKuuhSU6dC95QIW250xO769cdYGx9rWn3uBLTw2pUpud0Z5kVuMw9m9fqbNzGeuOU2HpyuZa+yUt2CTEDA==", + "dev": true, + "requires": { + "@webassemblyjs/helper-module-context": "1.5.12", + "@webassemblyjs/helper-wasm-bytecode": "1.5.12", + "@webassemblyjs/wast-parser": "1.5.12", + "debug": "^3.1.0", + "mamacro": "^0.0.3" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.5.12.tgz", + "integrity": "sha512-epTvkdwOIPpTE9edHS+V+shetYzpTbd91XOzUli1zAS0+NSgSe6ZsNggIqUNzhma1s4bN2f/m8c6B1NMdCERAg==", + "dev": true + }, + "@webassemblyjs/helper-api-error": { + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.5.12.tgz", + "integrity": "sha512-Goxag86JvLq8ucHLXFNSLYzf9wrR+CJr37DsESTAzSnGoqDTgw5eqiXSQVd/D9Biih7+DIn8UIQCxMs8emRRwg==", + "dev": true + }, + "@webassemblyjs/helper-buffer": { + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.5.12.tgz", + "integrity": "sha512-tJNUjttL5CxiiS/KLxT4/Zk0Nbl/poFhztFxktb46zoQEUWaGHR9ZJ0SnvE7DbFX5PY5JNJDMZ0Li4lm246fWw==", + "dev": true, + "requires": { + "debug": "^3.1.0" + } + }, + "@webassemblyjs/helper-code-frame": { + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.5.12.tgz", + "integrity": "sha512-0FrJgiST+MQDMvPigzs+UIk1vslLIqGadkEWdn53Lr0NsUC2JbheG9QaO3Zf6ycK2JwsHiUpGaMFcHYXStTPMA==", + "dev": true, + "requires": { + "@webassemblyjs/wast-printer": "1.5.12" + } + }, + "@webassemblyjs/helper-fsm": { + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.5.12.tgz", + "integrity": "sha512-QBHZ45VPUJ7UyYKvUFoaxrSS9H5hbkC9U7tdWgFHmnTMutkXSEgDg2gZg3I/QTsiKOCIwx4qJUJwPd7J4D5CNQ==", + "dev": true + }, + "@webassemblyjs/helper-module-context": { + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.5.12.tgz", + "integrity": "sha512-SCXR8hPI4JOG3cdy9HAO8W5/VQ68YXG/Hfs7qDf1cd64zWuMNshyEour5NYnLMVkrrtc0XzfVS/MdeV94woFHA==", + "dev": true, + "requires": { + "debug": "^3.1.0", + "mamacro": "^0.0.3" + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.5.12.tgz", + "integrity": "sha512-0Gz5lQcyvElNVbOTKwjEmIxGwdWf+zpAW/WGzGo95B7IgMEzyyfZU+PrGHDwiSH9c0knol9G7smQnY0ljrSA6g==", + "dev": true + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.5.12.tgz", + "integrity": "sha512-ge/CKVKBGpiJhFN9PIOQ7sPtGYJhxm/mW1Y3SpG1L6XBunfRz0YnLjW3TmhcOEFozIVyODPS1HZ9f7VR3GBGow==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.5.12", + "@webassemblyjs/helper-buffer": "1.5.12", + "@webassemblyjs/helper-wasm-bytecode": "1.5.12", + "@webassemblyjs/wasm-gen": "1.5.12", + "debug": "^3.1.0" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.5.12.tgz", + "integrity": "sha512-F+PEv9QBzPi1ThLBouUJbuxhEr+Sy/oua1ftXFKHiaYYS5Z9tKPvK/hgCxlSdq+RY4MSG15jU2JYb/K5pkoybg==", + "dev": true, + "requires": { + "ieee754": "^1.1.11" + } + }, + "@webassemblyjs/leb128": { + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.5.12.tgz", + "integrity": "sha512-cCOx/LVGiWyCwVrVlvGmTdnwHzIP4+zflLjGkZxWpYCpdNax9krVIJh1Pm7O86Ox/c5PrJpbvZU1cZLxndlPEw==", + "dev": true, + "requires": { + "leb": "^0.3.0" + } + }, + "@webassemblyjs/utf8": { + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.5.12.tgz", + "integrity": "sha512-FX8NYQMiTRU0TfK/tJVntsi9IEKsedSsna8qtsndWVE0x3zLndugiApxdNMIOoElBV9o4j0BUqR+iwU58QfPxQ==", + "dev": true + }, + "@webassemblyjs/wasm-edit": { + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.5.12.tgz", + "integrity": "sha512-r/oZAyC4EZl0ToOYJgvj+b0X6gVEKQMLT34pNNbtvWBehQOnaSXvVUA5FIYlH8ubWjFNAFqYaVGgQTjR1yuJdQ==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.5.12", + "@webassemblyjs/helper-buffer": "1.5.12", + "@webassemblyjs/helper-wasm-bytecode": "1.5.12", + "@webassemblyjs/helper-wasm-section": "1.5.12", + "@webassemblyjs/wasm-gen": "1.5.12", + "@webassemblyjs/wasm-opt": "1.5.12", + "@webassemblyjs/wasm-parser": "1.5.12", + "@webassemblyjs/wast-printer": "1.5.12", + "debug": "^3.1.0" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.5.12.tgz", + "integrity": "sha512-LTu+cr1YRxGGiVIXWhei/35lXXEwTnQU18x4V/gE+qCSJN21QcVTMjJuasTUh8WtmBZtOlqJbOQIeN7fGnHWhg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.5.12", + "@webassemblyjs/helper-wasm-bytecode": "1.5.12", + "@webassemblyjs/ieee754": "1.5.12", + "@webassemblyjs/leb128": "1.5.12", + "@webassemblyjs/utf8": "1.5.12" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.5.12.tgz", + "integrity": "sha512-LBwG5KPA9u/uigZVyTsDpS3CVxx3AePCnTItVL+OPkRCp5LqmLsOp4a3/c5CQE0Lecm0Ss9hjUTDcbYFZkXlfQ==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.5.12", + "@webassemblyjs/helper-buffer": "1.5.12", + "@webassemblyjs/wasm-gen": "1.5.12", + "@webassemblyjs/wasm-parser": "1.5.12", + "debug": "^3.1.0" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.5.12.tgz", + "integrity": "sha512-xset3+1AtoFYEfMg30nzCGBnhKmTBzbIKvMyLhqJT06TvYV+kA884AOUpUvhSmP6XPF3G+HVZPm/PbCGxH4/VQ==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.5.12", + "@webassemblyjs/helper-api-error": "1.5.12", + "@webassemblyjs/helper-wasm-bytecode": "1.5.12", + "@webassemblyjs/ieee754": "1.5.12", + "@webassemblyjs/leb128": "1.5.12", + "@webassemblyjs/utf8": "1.5.12" + } + }, + "@webassemblyjs/wast-parser": { + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.5.12.tgz", + "integrity": "sha512-QWUtzhvfY7Ue9GlJ3HeOB6w5g9vNYUUnG+Y96TWPkFHJTxZlcvGfNrUoACCw6eDb9gKaHrjt77aPq41a7y8svg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.5.12", + "@webassemblyjs/floating-point-hex-parser": "1.5.12", + "@webassemblyjs/helper-api-error": "1.5.12", + "@webassemblyjs/helper-code-frame": "1.5.12", + "@webassemblyjs/helper-fsm": "1.5.12", + "long": "^3.2.0", + "mamacro": "^0.0.3" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.5.12.tgz", + "integrity": "sha512-XF9RTeckFgDyl196uRKZWHFFfbkzsMK96QTXp+TC0R9gsV9DMiDGMSIllgy/WdrZ3y3dsQp4fTA5r4GoaOBchA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.5.12", + "@webassemblyjs/wast-parser": "1.5.12", + "long": "^3.2.0" + } + }, + "JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, + "requires": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + } + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "dev": true, + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "accessory": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/accessory/-/accessory-1.0.1.tgz", + "integrity": "sha1-JZVKJYKRohsb6PGQIGTpYs7mzmI=", + "dev": true, + "requires": { + "dot-parts": "~1.0.0" + } + }, + "acorn": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.2.1.tgz", + "integrity": "sha512-JD0xT5FCRDNyjDda3Lrg/IxFscp9q4tiYtxE1/nOzlKCk7hIRuYjhq1kCNkbPjMRMZuFq20HNQn1I9k8Oj0E+Q==", + "dev": true + }, + "acorn-dynamic-import": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz", + "integrity": "sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw==", + "dev": true + }, + "acorn-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", + "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", + "dev": true, + "requires": { + "acorn": "^3.0.4" + }, + "dependencies": { + "acorn": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", + "dev": true + } + } + }, + "acorn-node": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.7.0.tgz", + "integrity": "sha512-XhahLSsCB6X6CJbe+uNu3Mn9sJBNFxtBN9NLgAOQovfS6Kh0lDUtmlclhjn9CvEK7A7YyRU13PXlNcpSiLI9Yw==", + "dev": true, + "requires": { + "acorn": "^6.1.1", + "acorn-dynamic-import": "^4.0.0", + "acorn-walk": "^6.1.1", + "xtend": "^4.0.1" + } + }, + "acorn-walk": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", + "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==", + "dev": true + }, + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "dev": true, + "requires": { + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" + } + }, + "ajv-keywords": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", + "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==", + "dev": true + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "dev": true + }, + "ansi-align": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", + "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", + "dev": true, + "requires": { + "string-width": "^2.0.0" + } + }, + "ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", + "dev": true + }, + "ansi-gray": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz", + "integrity": "sha1-KWLPVOyXksSFEKPetSRDaGHvclE=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "ansi-wrap": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", + "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=", + "dev": true + }, + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + } + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true + }, + "archive-type": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/archive-type/-/archive-type-3.2.0.tgz", + "integrity": "sha1-nNnABpV+vpX62tW9YJiUKoE3N/Y=", + "dev": true, + "optional": true, + "requires": { + "file-type": "^3.1.0" + } + }, + "archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", + "dev": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "dev": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true + }, + "array-differ": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz", + "integrity": "sha1-7/UuN1gknTO+QCuLuOVkuytdQDE=", + "dev": true + }, + "array-filter": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz", + "integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=", + "dev": true + }, + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", + "dev": true + }, + "array-map": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz", + "integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=", + "dev": true + }, + "array-reduce": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz", + "integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=", + "dev": true + }, + "array-union": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-0.1.0.tgz", + "integrity": "sha1-7emAiDMGZeaZ4evwIny8YDTmJ9s=", + "dev": true, + "requires": { + "array-uniq": "^0.1.0" + }, + "dependencies": { + "array-uniq": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-0.1.1.tgz", + "integrity": "sha1-WGHz7U5LthdVl6TgeOiqeOvpWMc=", + "dev": true + } + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "dev": true, + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "assert": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.3.0.tgz", + "integrity": "sha1-A5OaYiWCqBLMICMgoLmlbJuBWEk=", + "dev": true, + "requires": { + "util": "0.10.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "dev": true + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "dev": true, + "requires": { + "inherits": "2.0.1" + } + } + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true + }, + "astw": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/astw/-/astw-2.2.0.tgz", + "integrity": "sha1-e9QXhNMkk5h66yOba04cV6hzuRc=", + "dev": true, + "requires": { + "acorn": "^4.0.3" + }, + "dependencies": { + "acorn": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", + "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=", + "dev": true + } + } + }, + "async": { + "version": "0.1.22", + "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz", + "integrity": "sha1-D8GqoIig4+8Ovi2IMbqw3PiEUGE=", + "dev": true + }, + "async-each": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", + "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", + "dev": true + }, + "async-each-series": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/async-each-series/-/async-each-series-1.1.0.tgz", + "integrity": "sha1-9C/YFV048hpbjqB8KOBj7RcAsTg=", + "dev": true, + "optional": true + }, + "async-foreach": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", + "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true + }, + "aws-sdk": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-1.12.0.tgz", + "integrity": "sha1-Y1tCY310O2LMeVk1cU2VXBJ2XrQ=", + "dev": true, + "requires": { + "xml2js": "0.2.4", + "xmlbuilder": "0.4.2" + } + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "dev": true + }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", + "dev": true + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + } + }, + "babel-core": { + "version": "6.26.3", + "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz", + "integrity": "sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==", + "dev": true, + "requires": { + "babel-code-frame": "^6.26.0", + "babel-generator": "^6.26.0", + "babel-helpers": "^6.24.1", + "babel-messages": "^6.23.0", + "babel-register": "^6.26.0", + "babel-runtime": "^6.26.0", + "babel-template": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "convert-source-map": "^1.5.1", + "debug": "^2.6.9", + "json5": "^0.5.1", + "lodash": "^4.17.4", + "minimatch": "^3.0.4", + "path-is-absolute": "^1.0.1", + "private": "^0.1.8", + "slash": "^1.0.0", + "source-map": "^0.5.7" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "babel-generator": { + "version": "6.26.1", + "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", + "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", + "dev": true, + "requires": { + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "detect-indent": "^4.0.0", + "jsesc": "^1.3.0", + "lodash": "^4.17.4", + "source-map": "^0.5.7", + "trim-right": "^1.0.1" + } + }, + "babel-helper-builder-binary-assignment-operator-visitor": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz", + "integrity": "sha1-zORReto1b0IgvK6KAsKzRvmlZmQ=", + "dev": true, + "requires": { + "babel-helper-explode-assignable-expression": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-helper-call-delegate": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz", + "integrity": "sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340=", + "dev": true, + "requires": { + "babel-helper-hoist-variables": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-helper-define-map": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz", + "integrity": "sha1-pfVtq0GiX5fstJjH66ypgZ+Vvl8=", + "dev": true, + "requires": { + "babel-helper-function-name": "^6.24.1", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "lodash": "^4.17.4" + } + }, + "babel-helper-explode-assignable-expression": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz", + "integrity": "sha1-8luCz33BBDPFX3BZLVdGQArCLKo=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-helper-function-name": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", + "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=", + "dev": true, + "requires": { + "babel-helper-get-function-arity": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-helper-get-function-arity": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", + "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-helper-hoist-variables": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz", + "integrity": "sha1-HssnaJydJVE+rbyZFKc/VAi+enY=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-helper-optimise-call-expression": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz", + "integrity": "sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-helper-regex": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz", + "integrity": "sha1-MlxZ+QL4LyS3T6zu0DY5VPZJXnI=", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "lodash": "^4.17.4" + } + }, + "babel-helper-remap-async-to-generator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz", + "integrity": "sha1-XsWBgnrXI/7N04HxySg5BnbkVRs=", + "dev": true, + "requires": { + "babel-helper-function-name": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-helper-replace-supers": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz", + "integrity": "sha1-v22/5Dk40XNpohPKiov3S2qQqxo=", + "dev": true, + "requires": { + "babel-helper-optimise-call-expression": "^6.24.1", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-helpers": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", + "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-loader": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-7.1.5.tgz", + "integrity": "sha512-iCHfbieL5d1LfOQeeVJEUyD9rTwBcP/fcEbRCfempxTDuqrKpu0AZjLAQHEQa3Yqyj9ORKe2iHfoj4rHLf7xpw==", + "dev": true, + "requires": { + "find-cache-dir": "^1.0.0", + "loader-utils": "^1.0.2", + "mkdirp": "^0.5.1" + } + }, + "babel-messages": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-check-es2015-constants": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz", + "integrity": "sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-syntax-async-functions": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz", + "integrity": "sha1-ytnK0RkbWtY0vzCuCHI5HgZHvpU=", + "dev": true + }, + "babel-plugin-syntax-exponentiation-operator": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz", + "integrity": "sha1-nufoM3KQ2pUoggGmpX9BcDF4MN4=", + "dev": true + }, + "babel-plugin-syntax-object-rest-spread": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", + "integrity": "sha1-/WU28rzhODb/o6VFjEkDpZe7O/U=", + "dev": true + }, + "babel-plugin-syntax-trailing-function-commas": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz", + "integrity": "sha1-ugNgk3+NBuQBgKQ/4NVhb/9TLPM=", + "dev": true + }, + "babel-plugin-transform-async-to-generator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz", + "integrity": "sha1-ZTbjeK/2yx1VF6wOQOs+n8jQh2E=", + "dev": true, + "requires": { + "babel-helper-remap-async-to-generator": "^6.24.1", + "babel-plugin-syntax-async-functions": "^6.8.0", + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-arrow-functions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz", + "integrity": "sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-block-scoped-functions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz", + "integrity": "sha1-u8UbSflk1wy42OC5ToICRs46YUE=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-block-scoping": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz", + "integrity": "sha1-1w9SmcEwjQXBL0Y4E7CgnnOxiV8=", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "babel-template": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "lodash": "^4.17.4" + } + }, + "babel-plugin-transform-es2015-classes": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz", + "integrity": "sha1-WkxYpQyclGHlZLSyo7+ryXolhNs=", + "dev": true, + "requires": { + "babel-helper-define-map": "^6.24.1", + "babel-helper-function-name": "^6.24.1", + "babel-helper-optimise-call-expression": "^6.24.1", + "babel-helper-replace-supers": "^6.24.1", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-computed-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz", + "integrity": "sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-destructuring": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz", + "integrity": "sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-duplicate-keys": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz", + "integrity": "sha1-c+s9MQypaePvnskcU3QabxV2Qj4=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-for-of": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz", + "integrity": "sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-function-name": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz", + "integrity": "sha1-g0yJhTvDaxrw86TF26qU/Y6sqos=", + "dev": true, + "requires": { + "babel-helper-function-name": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-literals": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz", + "integrity": "sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-modules-amd": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz", + "integrity": "sha1-Oz5UAXI5hC1tGcMBHEvS8AoA0VQ=", + "dev": true, + "requires": { + "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-modules-commonjs": { + "version": "6.26.2", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz", + "integrity": "sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q==", + "dev": true, + "requires": { + "babel-plugin-transform-strict-mode": "^6.24.1", + "babel-runtime": "^6.26.0", + "babel-template": "^6.26.0", + "babel-types": "^6.26.0" + } + }, + "babel-plugin-transform-es2015-modules-systemjs": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz", + "integrity": "sha1-/4mhQrkRmpBhlfXxBuzzBdlAfSM=", + "dev": true, + "requires": { + "babel-helper-hoist-variables": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-modules-umd": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz", + "integrity": "sha1-rJl+YoXNGO1hdq22B9YCNErThGg=", + "dev": true, + "requires": { + "babel-plugin-transform-es2015-modules-amd": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-object-super": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz", + "integrity": "sha1-JM72muIcuDp/hgPa0CH1cusnj40=", + "dev": true, + "requires": { + "babel-helper-replace-supers": "^6.24.1", + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-parameters": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz", + "integrity": "sha1-V6w1GrScrxSpfNE7CfZv3wpiXys=", + "dev": true, + "requires": { + "babel-helper-call-delegate": "^6.24.1", + "babel-helper-get-function-arity": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-shorthand-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz", + "integrity": "sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-spread": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz", + "integrity": "sha1-1taKmfia7cRTbIGlQujdnxdG+NE=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-sticky-regex": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz", + "integrity": "sha1-AMHNsaynERLN8M9hJsLta0V8zbw=", + "dev": true, + "requires": { + "babel-helper-regex": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-template-literals": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz", + "integrity": "sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-typeof-symbol": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz", + "integrity": "sha1-3sCfHN3/lLUqxz1QXITfWdzOs3I=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-unicode-regex": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz", + "integrity": "sha1-04sS9C6nMj9yk4fxinxa4frrNek=", + "dev": true, + "requires": { + "babel-helper-regex": "^6.24.1", + "babel-runtime": "^6.22.0", + "regexpu-core": "^2.0.0" + } + }, + "babel-plugin-transform-exponentiation-operator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz", + "integrity": "sha1-KrDJx/MJj6SJB3cruBP+QejeOg4=", + "dev": true, + "requires": { + "babel-helper-builder-binary-assignment-operator-visitor": "^6.24.1", + "babel-plugin-syntax-exponentiation-operator": "^6.8.0", + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-object-rest-spread": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz", + "integrity": "sha1-DzZpLVD+9rfi1LOsFHgTepY7ewY=", + "dev": true, + "requires": { + "babel-plugin-syntax-object-rest-spread": "^6.8.0", + "babel-runtime": "^6.26.0" + } + }, + "babel-plugin-transform-regenerator": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz", + "integrity": "sha1-4HA2lvveJ/Cj78rPi03KL3s6jy8=", + "dev": true, + "requires": { + "regenerator-transform": "^0.10.0" + } + }, + "babel-plugin-transform-strict-mode": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz", + "integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-preset-env": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/babel-preset-env/-/babel-preset-env-1.7.0.tgz", + "integrity": "sha512-9OR2afuKDneX2/q2EurSftUYM0xGu4O2D9adAhVfADDhrYDaxXV0rBbevVYoY9n6nyX1PmQW/0jtpJvUNr9CHg==", + "dev": true, + "requires": { + "babel-plugin-check-es2015-constants": "^6.22.0", + "babel-plugin-syntax-trailing-function-commas": "^6.22.0", + "babel-plugin-transform-async-to-generator": "^6.22.0", + "babel-plugin-transform-es2015-arrow-functions": "^6.22.0", + "babel-plugin-transform-es2015-block-scoped-functions": "^6.22.0", + "babel-plugin-transform-es2015-block-scoping": "^6.23.0", + "babel-plugin-transform-es2015-classes": "^6.23.0", + "babel-plugin-transform-es2015-computed-properties": "^6.22.0", + "babel-plugin-transform-es2015-destructuring": "^6.23.0", + "babel-plugin-transform-es2015-duplicate-keys": "^6.22.0", + "babel-plugin-transform-es2015-for-of": "^6.23.0", + "babel-plugin-transform-es2015-function-name": "^6.22.0", + "babel-plugin-transform-es2015-literals": "^6.22.0", + "babel-plugin-transform-es2015-modules-amd": "^6.22.0", + "babel-plugin-transform-es2015-modules-commonjs": "^6.23.0", + "babel-plugin-transform-es2015-modules-systemjs": "^6.23.0", + "babel-plugin-transform-es2015-modules-umd": "^6.23.0", + "babel-plugin-transform-es2015-object-super": "^6.22.0", + "babel-plugin-transform-es2015-parameters": "^6.23.0", + "babel-plugin-transform-es2015-shorthand-properties": "^6.22.0", + "babel-plugin-transform-es2015-spread": "^6.22.0", + "babel-plugin-transform-es2015-sticky-regex": "^6.22.0", + "babel-plugin-transform-es2015-template-literals": "^6.22.0", + "babel-plugin-transform-es2015-typeof-symbol": "^6.23.0", + "babel-plugin-transform-es2015-unicode-regex": "^6.22.0", + "babel-plugin-transform-exponentiation-operator": "^6.22.0", + "babel-plugin-transform-regenerator": "^6.22.0", + "browserslist": "^3.2.6", + "invariant": "^2.2.2", + "semver": "^5.3.0" + } + }, + "babel-preset-es2015": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz", + "integrity": "sha1-1EBQ1rwsn+6nAqrzjXJ6AhBTiTk=", + "dev": true, + "requires": { + "babel-plugin-check-es2015-constants": "^6.22.0", + "babel-plugin-transform-es2015-arrow-functions": "^6.22.0", + "babel-plugin-transform-es2015-block-scoped-functions": "^6.22.0", + "babel-plugin-transform-es2015-block-scoping": "^6.24.1", + "babel-plugin-transform-es2015-classes": "^6.24.1", + "babel-plugin-transform-es2015-computed-properties": "^6.24.1", + "babel-plugin-transform-es2015-destructuring": "^6.22.0", + "babel-plugin-transform-es2015-duplicate-keys": "^6.24.1", + "babel-plugin-transform-es2015-for-of": "^6.22.0", + "babel-plugin-transform-es2015-function-name": "^6.24.1", + "babel-plugin-transform-es2015-literals": "^6.22.0", + "babel-plugin-transform-es2015-modules-amd": "^6.24.1", + "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1", + "babel-plugin-transform-es2015-modules-systemjs": "^6.24.1", + "babel-plugin-transform-es2015-modules-umd": "^6.24.1", + "babel-plugin-transform-es2015-object-super": "^6.24.1", + "babel-plugin-transform-es2015-parameters": "^6.24.1", + "babel-plugin-transform-es2015-shorthand-properties": "^6.24.1", + "babel-plugin-transform-es2015-spread": "^6.22.0", + "babel-plugin-transform-es2015-sticky-regex": "^6.24.1", + "babel-plugin-transform-es2015-template-literals": "^6.22.0", + "babel-plugin-transform-es2015-typeof-symbol": "^6.22.0", + "babel-plugin-transform-es2015-unicode-regex": "^6.24.1", + "babel-plugin-transform-regenerator": "^6.24.1" + } + }, + "babel-register": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", + "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=", + "dev": true, + "requires": { + "babel-core": "^6.26.0", + "babel-runtime": "^6.26.0", + "core-js": "^2.5.0", + "home-or-tmp": "^2.0.0", + "lodash": "^4.17.4", + "mkdirp": "^0.5.1", + "source-map-support": "^0.4.15" + }, + "dependencies": { + "source-map-support": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", + "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", + "dev": true, + "requires": { + "source-map": "^0.5.6" + } + } + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "dev": true, + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + } + }, + "babel-template": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", + "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "lodash": "^4.17.4" + } + }, + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", + "dev": true, + "requires": { + "babel-code-frame": "^6.26.0", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "debug": "^2.6.8", + "globals": "^9.18.0", + "invariant": "^2.2.2", + "lodash": "^4.17.4" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + }, + "babelify": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/babelify/-/babelify-7.3.0.tgz", + "integrity": "sha1-qlau3nBn/XvVSWZu4W3ChQh+iOU=", + "dev": true, + "requires": { + "babel-core": "^6.0.14", + "object-assign": "^4.0.0" + } + }, + "babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", + "dev": true + }, + "backbone": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/backbone/-/backbone-1.2.3.tgz", + "integrity": "sha1-wiz9B/yG676uYdGJKe0RXpmdZbk=", + "requires": { + "underscore": ">=1.7.0" + } + }, + "backbone-poller": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/backbone-poller/-/backbone-poller-1.1.4.tgz", + "integrity": "sha512-h+2XFKG0LhsB2dd5cWowV+ySyn46loPk5YL3DWdnRI34KbruI2P4ZNJh/qCl90n3rkAv1laEj4lfSUsbiWjDNQ==", + "requires": { + "backbone": ">=0.9.*", + "underscore": "*" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "base64-js": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", + "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==", + "dev": true + }, + "basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + }, + "batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "dev": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "beeper": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/beeper/-/beeper-1.1.1.tgz", + "integrity": "sha1-5tXqjF2tABMEpwsiY4RH9pyy+Ak=", + "dev": true + }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, + "bin-build": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/bin-build/-/bin-build-2.2.0.tgz", + "integrity": "sha1-EfjdYfcP/Por3KpbRvXo/t1CIcw=", + "dev": true, + "optional": true, + "requires": { + "archive-type": "^3.0.1", + "decompress": "^3.0.0", + "download": "^4.1.2", + "exec-series": "^1.0.0", + "rimraf": "^2.2.6", + "tempfile": "^1.0.0", + "url-regex": "^3.0.0" + } + }, + "bin-check": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bin-check/-/bin-check-2.0.0.tgz", + "integrity": "sha1-hvjm9CU4k99g3DFpV/WvAqywWTA=", + "dev": true, + "optional": true, + "requires": { + "executable": "^1.0.0" + } + }, + "bin-version": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/bin-version/-/bin-version-1.0.4.tgz", + "integrity": "sha1-nrSY7m/Xb3q5p8FgQ2+JV5Q1144=", + "dev": true, + "optional": true, + "requires": { + "find-versions": "^1.0.0" + } + }, + "bin-version-check": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/bin-version-check/-/bin-version-check-2.1.0.tgz", + "integrity": "sha1-5OXfKQuQaffRETJAMe/BP90RpbA=", + "dev": true, + "optional": true, + "requires": { + "bin-version": "^1.0.0", + "minimist": "^1.1.0", + "semver": "^4.0.3", + "semver-truncate": "^1.0.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true, + "optional": true + }, + "semver": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz", + "integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto=", + "dev": true, + "optional": true + } + } + }, + "bin-wrapper": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/bin-wrapper/-/bin-wrapper-3.0.2.tgz", + "integrity": "sha1-Z9MwYmLksaXy+I7iNGT2plVneus=", + "dev": true, + "optional": true, + "requires": { + "bin-check": "^2.0.0", + "bin-version-check": "^2.1.0", + "download": "^4.0.0", + "each-async": "^1.1.1", + "lazy-req": "^1.0.0", + "os-filter-obj": "^1.0.0" + } + }, + "binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "dev": true + }, + "bl": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", + "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", + "dev": true, + "optional": true, + "requires": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, + "block-stream": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "dev": true, + "requires": { + "inherits": "~2.0.0" + } + }, + "bluebird": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.5.tgz", + "integrity": "sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==", + "dev": true + }, + "bn.js": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", + "dev": true + }, + "boxen": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", + "integrity": "sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==", + "dev": true, + "requires": { + "ansi-align": "^2.0.0", + "camelcase": "^4.0.0", + "chalk": "^2.0.1", + "cli-boxes": "^1.0.0", + "string-width": "^2.0.0", + "term-size": "^1.2.0", + "widest-line": "^2.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", + "dev": true + }, + "browser-pack": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/browser-pack/-/browser-pack-6.1.0.tgz", + "integrity": "sha512-erYug8XoqzU3IfcU8fUgyHqyOXqIE4tUTTQ+7mqUjQlvnXkOO6OlT9c/ZoJVHYoAaqGxr09CN53G7XIsO4KtWA==", + "dev": true, + "requires": { + "JSONStream": "^1.0.3", + "combine-source-map": "~0.8.0", + "defined": "^1.0.0", + "safe-buffer": "^5.1.1", + "through2": "^2.0.0", + "umd": "^3.0.0" + } + }, + "browser-resolve": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz", + "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==", + "dev": true, + "requires": { + "resolve": "1.1.7" + }, + "dependencies": { + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + } + } + }, + "browserify": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/browserify/-/browserify-13.0.0.tgz", + "integrity": "sha1-jyI7sk/07kM15r6pZx3ilOQ7pqM=", + "dev": true, + "requires": { + "JSONStream": "^1.0.3", + "assert": "~1.3.0", + "browser-pack": "^6.0.1", + "browser-resolve": "^1.11.0", + "browserify-zlib": "~0.1.2", + "buffer": "^4.1.0", + "concat-stream": "~1.5.1", + "console-browserify": "^1.1.0", + "constants-browserify": "~1.0.0", + "crypto-browserify": "^3.0.0", + "defined": "^1.0.0", + "deps-sort": "^2.0.0", + "domain-browser": "~1.1.0", + "duplexer2": "~0.1.2", + "events": "~1.1.0", + "glob": "^5.0.15", + "has": "^1.0.0", + "htmlescape": "^1.1.0", + "https-browserify": "~0.0.0", + "inherits": "~2.0.1", + "insert-module-globals": "^7.0.0", + "isarray": "0.0.1", + "labeled-stream-splicer": "^2.0.0", + "module-deps": "^4.0.2", + "os-browserify": "~0.1.1", + "parents": "^1.0.1", + "path-browserify": "~0.0.0", + "process": "~0.11.0", + "punycode": "^1.3.2", + "querystring-es3": "~0.2.0", + "read-only-stream": "^2.0.0", + "readable-stream": "^2.0.2", + "resolve": "^1.1.4", + "shasum": "^1.0.0", + "shell-quote": "^1.4.3", + "stream-browserify": "^2.0.0", + "stream-http": "^2.0.0", + "string_decoder": "~0.10.0", + "subarg": "^1.0.0", + "syntax-error": "^1.1.1", + "through2": "^2.0.0", + "timers-browserify": "^1.0.1", + "tty-browserify": "~0.0.0", + "url": "~0.11.0", + "util": "~0.10.1", + "vm-browserify": "~0.0.1", + "xtend": "^4.0.0" + } + }, + "browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "dev": true, + "requires": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "dev": true, + "requires": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "browserify-rsa": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "randombytes": "^2.0.1" + } + }, + "browserify-shim": { + "version": "3.8.12", + "resolved": "https://registry.npmjs.org/browserify-shim/-/browserify-shim-3.8.12.tgz", + "integrity": "sha1-4sl6E0xesSLiu09hcHoH9vc/8JI=", + "dev": true, + "requires": { + "exposify": "~0.4.3", + "mothership": "~0.2.0", + "rename-function-calls": "~0.1.0", + "resolve": "~0.6.1", + "through": "~2.3.4" + }, + "dependencies": { + "resolve": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-0.6.3.tgz", + "integrity": "sha1-3ZV5gufnNt699TtYpN2RdUV13UY=", + "dev": true + } + } + }, + "browserify-sign": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", + "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", + "dev": true, + "requires": { + "bn.js": "^4.1.1", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.2", + "elliptic": "^6.0.0", + "inherits": "^2.0.1", + "parse-asn1": "^5.0.0" + } + }, + "browserify-zlib": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz", + "integrity": "sha1-uzX4pRn2AOD6a4SFJByXnQFB+y0=", + "dev": true, + "requires": { + "pako": "~0.2.0" + } + }, + "browserslist": { + "version": "3.2.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-3.2.8.tgz", + "integrity": "sha512-WHVocJYavUwVgVViC0ORikPHQquXwVh939TaelZ4WDqpWgTX/FsGhl/+P4qBUAGcRvtOgDgC+xftNWWp2RUTAQ==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30000844", + "electron-to-chromium": "^1.3.47" + } + }, + "buffer": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "dev": true, + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + } + } + }, + "buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "dev": true, + "optional": true, + "requires": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "dev": true, + "optional": true + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", + "dev": true, + "optional": true + }, + "buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=", + "dev": true, + "optional": true + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "buffer-to-vinyl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-to-vinyl/-/buffer-to-vinyl-1.1.0.tgz", + "integrity": "sha1-APFfruOreh3aLN5tkSG//dB7ImI=", + "dev": true, + "requires": { + "file-type": "^3.1.0", + "readable-stream": "^2.0.2", + "uuid": "^2.0.1", + "vinyl": "^1.0.0" + } + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", + "dev": true + }, + "bufferstreams": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/bufferstreams/-/bufferstreams-0.0.2.tgz", + "integrity": "sha1-fOjf+Wi7rAC56QFYosQUVvdAq90=", + "dev": true, + "requires": { + "readable-stream": "^1.0.26-2" + }, + "dependencies": { + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + } + } + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", + "dev": true + }, + "builtins": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-0.0.7.tgz", + "integrity": "sha1-NVIZzWzxjb58Acx/0tznZc/cVJo=", + "dev": true + }, + "cacache": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-10.0.4.tgz", + "integrity": "sha512-Dph0MzuH+rTQzGPNT9fAnrPmMmjKfST6trxJeK7NQuHRaVw24VzPRWTmg9MpcwOVQZO0E1FBICUlFeNaKPIfHA==", + "dev": true, + "requires": { + "bluebird": "^3.5.1", + "chownr": "^1.0.1", + "glob": "^7.1.2", + "graceful-fs": "^4.1.11", + "lru-cache": "^4.1.1", + "mississippi": "^2.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.2", + "ssri": "^5.2.4", + "unique-filename": "^1.1.0", + "y18n": "^4.0.0" + }, + "dependencies": { + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + } + } + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, + "cached-path-relative": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.0.2.tgz", + "integrity": "sha512-5r2GqsoEb4qMTTN9J+WzXfjov+hjxT+j3u5K+kIVNIwAd99DLCJE9pBIMP1qVeybV6JiijL385Oz0DcYxfbOIg==", + "dev": true + }, + "caller-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", + "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", + "dev": true, + "requires": { + "callsites": "^0.2.0" + } + }, + "callsites": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", + "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", + "dev": true + }, + "camel-case": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-1.2.2.tgz", + "integrity": "sha1-Gsp8TRlTWaLOmVV5NDPG5VQlEfI=", + "dev": true, + "requires": { + "sentence-case": "^1.1.1", + "upper-case": "^1.1.1" + } + }, + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", + "dev": true + }, + "camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "dev": true, + "requires": { + "camelcase": "^2.0.0", + "map-obj": "^1.0.0" + } + }, + "camshaft-reference": { + "version": "0.34.0", + "resolved": "https://registry.npmjs.org/camshaft-reference/-/camshaft-reference-0.34.0.tgz", + "integrity": "sha512-oz/NyTrgQBgWBDEbjC/s00XWp/O6qxjOJpNffSlNuT30BvGNsEmfWAgMThOD9SzDkuVgTJbHZGlBr4vkYt5Bew==" + }, + "caniuse-lite": { + "version": "1.0.30000988", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000988.tgz", + "integrity": "sha512-lPj3T8poYrRc/bniW5SQPND3GRtSrQdUM/R4mCYTbZxyi3jQiggLvZH4+BYUuX0t4TXjU+vMM7KFDQg+rSzZUQ==", + "dev": true + }, + "capture-stack-trace": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz", + "integrity": "sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw==", + "dev": true + }, + "carto": { + "version": "github:cartodb/carto#85881d99dd7fcf2c4e16478b04db67108d27a50c", + "from": "github:cartodb/carto#master", + "requires": { + "mapnik-reference": "~6.0.2", + "optimist": "~0.6.0", + "underscore": "1.8.3" + } + }, + "cartoassets": { + "version": "github:CartoDB/CartoAssets#6b9d64fafdd4ddc7cd7e249d3932217902802369", + "from": "github:CartoDB/CartoAssets#master", + "dev": true + }, + "cartocolor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cartocolor/-/cartocolor-4.0.0.tgz", + "integrity": "sha1-hBoyIti1sicY2dVFseW5cssm6zY=", + "requires": { + "colorbrewer": "1.0.0" + } + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true + }, + "catharsis": { + "version": "0.8.11", + "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.8.11.tgz", + "integrity": "sha512-a+xUyMV7hD1BrDQA/3iPV7oc+6W26BgVJO05PGEoatMyIuPScQKsde6i3YorWX1qs+AZjnJ18NqdKoCtKiNh1g==", + "dev": true, + "requires": { + "lodash": "^4.17.14" + } + }, + "caw": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/caw/-/caw-1.2.0.tgz", + "integrity": "sha1-/7Im/n78VHKI3GLuPpcHPCEtEDQ=", + "dev": true, + "optional": true, + "requires": { + "get-proxy": "^1.0.1", + "is-obj": "^1.0.0", + "object-assign": "^3.0.0", + "tunnel-agent": "^0.4.0" + }, + "dependencies": { + "object-assign": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", + "integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=", + "dev": true, + "optional": true + } + } + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "change-case": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-2.3.1.tgz", + "integrity": "sha1-LE/ePwY7tB0AzWjg1aCdthy+iU8=", + "dev": true, + "requires": { + "camel-case": "^1.1.1", + "constant-case": "^1.1.0", + "dot-case": "^1.1.0", + "is-lower-case": "^1.1.0", + "is-upper-case": "^1.1.0", + "lower-case": "^1.1.1", + "lower-case-first": "^1.0.0", + "param-case": "^1.1.0", + "pascal-case": "^1.1.0", + "path-case": "^1.1.0", + "sentence-case": "^1.1.1", + "snake-case": "^1.1.0", + "swap-case": "^1.1.0", + "title-case": "^1.1.0", + "upper-case": "^1.1.1", + "upper-case-first": "^1.1.0" + } + }, + "chardet": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", + "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=", + "dev": true + }, + "chokidar": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.6.tgz", + "integrity": "sha512-V2jUo67OKkc6ySiRpJrjlpJKl9kDuG+Xb8VgsGzb+aEouhgS1D0weyPU4lEzdAcsCAvrih2J2BqyXqHWvVLw5g==", + "dev": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + } + } + }, + "chownr": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.2.tgz", + "integrity": "sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A==", + "dev": true + }, + "chrome-trace-event": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz", + "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "ci-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", + "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==", + "dev": true + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "circular-json": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", + "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", + "dev": true + }, + "clap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/clap/-/clap-1.2.3.tgz", + "integrity": "sha512-4CoL/A3hf90V3VIEjeuhSvlGFEHKzOz+Wfc2IVZc+FaUgU0ZQafJTP49fvnULipOPcAfqhyI2duwQyns6xqjYA==", + "dev": true, + "optional": true, + "requires": { + "chalk": "^1.1.3" + } + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "clean-css": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-2.0.8.tgz", + "integrity": "sha1-6TfN/cxXgaAIF67EB56Fs+wVeiA=", + "dev": true, + "requires": { + "commander": "2.0.x" + } + }, + "clean-sketch": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/clean-sketch/-/clean-sketch-1.0.1.tgz", + "integrity": "sha1-i86BAmPro51StxsHBY/F5BmjbiM=", + "dev": true + }, + "cli": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/cli/-/cli-0.6.6.tgz", + "integrity": "sha1-Aq1Eo4Cr8nraxebwzdewQ9dMU+M=", + "dev": true, + "requires": { + "exit": "0.1.2", + "glob": "~ 3.2.1" + }, + "dependencies": { + "glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", + "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", + "dev": true, + "requires": { + "inherits": "2", + "minimatch": "0.3" + } + }, + "lru-cache": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", + "dev": true + }, + "minimatch": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", + "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", + "dev": true, + "requires": { + "lru-cache": "2", + "sigmund": "~1.0.0" + } + } + } + }, + "cli-boxes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", + "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=", + "dev": true + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "dev": true, + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "dev": true + }, + "clip-path-polygon": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/clip-path-polygon/-/clip-path-polygon-0.1.12.tgz", + "integrity": "sha1-mK+nZEHqbBJWfLELSg+kVrre2kU=", + "requires": { + "jquery": "2.1.4" + } + }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", + "dev": true + }, + "clone-stats": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-0.0.1.tgz", + "integrity": "sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE=", + "dev": true + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "coa": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/coa/-/coa-1.0.4.tgz", + "integrity": "sha1-qe8VNmDWqGqL3sAomlxoTSF0Mv0=", + "dev": true, + "optional": true, + "requires": { + "q": "^1.1.2" + } + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "coffee-script": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.3.3.tgz", + "integrity": "sha1-FQ1rTLUiiUNp7+1qIQHCC8f0pPQ=", + "dev": true + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true + }, + "colorbrewer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/colorbrewer/-/colorbrewer-1.0.0.tgz", + "integrity": "sha1-T5czO5abp2Ejgr5LwzlLNB+0yKI=" + }, + "colors": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", + "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=", + "dev": true + }, + "combine-source-map": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.8.0.tgz", + "integrity": "sha1-pY0N8ELBhvz4IqjoAV9UUNLXmos=", + "dev": true, + "requires": { + "convert-source-map": "~1.1.0", + "inline-source-map": "~0.6.0", + "lodash.memoize": "~3.0.3", + "source-map": "~0.5.3" + }, + "dependencies": { + "convert-source-map": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz", + "integrity": "sha1-SCnId+n+SbMWHzvzZziI4gRpmGA=", + "dev": true + } + } + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.0.0.tgz", + "integrity": "sha1-0bhvkB+LZL2UG96tr5JFMDk76Sg=", + "dev": true + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "concat-stream": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.2.tgz", + "integrity": "sha1-cIl4Yk2FavQaWnQd790mHadSwmY=", + "dev": true, + "requires": { + "inherits": "~2.0.1", + "readable-stream": "~2.0.0", + "typedarray": "~0.0.5" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", + "dev": true + }, + "readable-stream": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "string_decoder": "~0.10.x", + "util-deprecate": "~1.0.1" + } + } + } + }, + "configstore": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.2.tgz", + "integrity": "sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw==", + "dev": true, + "requires": { + "dot-prop": "^4.1.0", + "graceful-fs": "^4.1.2", + "make-dir": "^1.0.0", + "unique-string": "^1.0.0", + "write-file-atomic": "^2.0.0", + "xdg-basedir": "^3.0.0" + } + }, + "connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "requires": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "connect-livereload": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/connect-livereload/-/connect-livereload-0.5.4.tgz", + "integrity": "sha1-gBV9E3HJ83zBQDmrGJWXDRGdw7w=", + "dev": true + }, + "console-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", + "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", + "dev": true, + "requires": { + "date-now": "^0.1.4" + } + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "dev": true + }, + "console-stream": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/console-stream/-/console-stream-0.1.1.tgz", + "integrity": "sha1-oJX+B7IEZZVfL6/Si11yvM2UnUQ=", + "dev": true, + "optional": true + }, + "consolidate": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/consolidate/-/consolidate-0.10.0.tgz", + "integrity": "sha1-gfGmzroSR9+c73omHOUnws5Tj3o=", + "dev": true + }, + "constant-case": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-1.1.2.tgz", + "integrity": "sha1-jsLKW6ND4Aqjjb9OIA/VrJB+/WM=", + "dev": true, + "requires": { + "snake-case": "^1.1.0", + "upper-case": "^1.1.1" + } + }, + "constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", + "dev": true + }, + "contains-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", + "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", + "dev": true + }, + "convert-source-map": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", + "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "copy-concurrently": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", + "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", + "dev": true, + "requires": { + "aproba": "^1.1.1", + "fs-write-stream-atomic": "^1.0.8", + "iferr": "^0.1.5", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.0" + } + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true + }, + "core-js": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz", + "integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "create-ecdh": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", + "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "elliptic": "^6.0.0" + } + }, + "create-error-class": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", + "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", + "dev": true, + "requires": { + "capture-stack-trace": "^1.0.0" + } + }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "dev": true, + "requires": { + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0", + "randomfill": "^1.0.3" + } + }, + "crypto-random-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", + "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=", + "dev": true + }, + "csso": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/csso/-/csso-2.0.0.tgz", + "integrity": "sha1-F4tDpEYhIhwndWCG9THgL0KQDug=", + "dev": true, + "optional": true, + "requires": { + "clap": "^1.0.9", + "source-map": "^0.5.3" + } + }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "dev": true, + "requires": { + "array-find-index": "^1.0.1" + } + }, + "cyclist": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz", + "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=", + "dev": true + }, + "d3": { + "version": "3.5.17", + "resolved": "https://registry.npmjs.org/d3/-/d3-3.5.17.tgz", + "integrity": "sha1-vEZ0gAQ3iyGjYMn8fPUjF5B2L7g=" + }, + "d3-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.1.tgz", + "integrity": "sha512-CyINJQ0SOUHojDdFDH4JEM0552vCR1utGyLHegJHyYH0JyCpSeTPxi4OBqHMA2jJZq4NH782LtaJWBImqI/HBw==" + }, + "d3-format": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.2.0.tgz", + "integrity": "sha1-a0gLqohohdRlHcJIqPSsnaFtsHo=" + }, + "d3-time": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.0.11.tgz", + "integrity": "sha512-Z3wpvhPLW4vEScGeIMUckDW7+3hWKOQfAWg/U7PlWBnQmeKQ00gCUsTtWSYulrKNA7ta8hJ+xXc6MHrMuITwEw==" + }, + "d3-time-format": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.1.0.tgz", + "integrity": "sha512-mqTsfDTylgwE3YE/VNs9oB2OGX274fO0B5j1irbgLQI+X3FPoJg25pesNxrcdZ2nBeRx/6sHDJlDyMIjWL0BGQ==", + "requires": { + "d3-time": "1" + } + }, + "dash-ast": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dash-ast/-/dash-ast-1.0.0.tgz", + "integrity": "sha512-Vy4dx7gquTeMcQR/hDkYLGUnwVil6vk4FOOct+djUnHOUWt+zJPJAaRIXaAFkPXtJjvlY7o3rfRu0/3hpnwoUA==", + "dev": true + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "date-now": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", + "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", + "dev": true + }, + "date-time": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/date-time/-/date-time-0.1.1.tgz", + "integrity": "sha1-7S9tk9l5DOL9ZtW1/z7dW7y/Owc=", + "dev": true + }, + "dateformat": { + "version": "1.0.2-1.2.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.2-1.2.3.tgz", + "integrity": "sha1-sCIMAt6YYXQztyhRz0fePfLNvuk=", + "dev": true + }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, + "decompress": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/decompress/-/decompress-3.0.0.tgz", + "integrity": "sha1-rx3VDQbjv8QyRh033hGzjA2ZG+0=", + "dev": true, + "optional": true, + "requires": { + "buffer-to-vinyl": "^1.0.0", + "concat-stream": "^1.4.6", + "decompress-tar": "^3.0.0", + "decompress-tarbz2": "^3.0.0", + "decompress-targz": "^3.0.0", + "decompress-unzip": "^3.0.0", + "stream-combiner2": "^1.1.1", + "vinyl-assign": "^1.0.1", + "vinyl-fs": "^2.2.0" + } + }, + "decompress-tar": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-3.1.0.tgz", + "integrity": "sha1-IXx4n5uURQ76rcXF5TeXj8MzxGY=", + "dev": true, + "optional": true, + "requires": { + "is-tar": "^1.0.0", + "object-assign": "^2.0.0", + "strip-dirs": "^1.0.0", + "tar-stream": "^1.1.1", + "through2": "^0.6.1", + "vinyl": "^0.4.3" + }, + "dependencies": { + "clone": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/clone/-/clone-0.2.0.tgz", + "integrity": "sha1-xhJqkK1Pctv1rNskPMN3JP6T/B8=", + "dev": true, + "optional": true + }, + "object-assign": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-2.1.1.tgz", + "integrity": "sha1-Q8NuXVaf+OSBbE76i+AtJpZ8GKo=", + "dev": true, + "optional": true + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "through2": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", + "dev": true, + "optional": true, + "requires": { + "readable-stream": ">=1.0.33-1 <1.1.0-0", + "xtend": ">=4.0.0 <4.1.0-0" + } + }, + "vinyl": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.4.6.tgz", + "integrity": "sha1-LzVsh6VQolVGHza76ypbqL94SEc=", + "dev": true, + "optional": true, + "requires": { + "clone": "^0.2.0", + "clone-stats": "^0.0.1" + } + } + } + }, + "decompress-tarbz2": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-3.1.0.tgz", + "integrity": "sha1-iyOTVoE1X58YnYclag+L3ZbZZm0=", + "dev": true, + "optional": true, + "requires": { + "is-bzip2": "^1.0.0", + "object-assign": "^2.0.0", + "seek-bzip": "^1.0.3", + "strip-dirs": "^1.0.0", + "tar-stream": "^1.1.1", + "through2": "^0.6.1", + "vinyl": "^0.4.3" + }, + "dependencies": { + "clone": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/clone/-/clone-0.2.0.tgz", + "integrity": "sha1-xhJqkK1Pctv1rNskPMN3JP6T/B8=", + "dev": true, + "optional": true + }, + "object-assign": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-2.1.1.tgz", + "integrity": "sha1-Q8NuXVaf+OSBbE76i+AtJpZ8GKo=", + "dev": true, + "optional": true + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "through2": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", + "dev": true, + "optional": true, + "requires": { + "readable-stream": ">=1.0.33-1 <1.1.0-0", + "xtend": ">=4.0.0 <4.1.0-0" + } + }, + "vinyl": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.4.6.tgz", + "integrity": "sha1-LzVsh6VQolVGHza76ypbqL94SEc=", + "dev": true, + "optional": true, + "requires": { + "clone": "^0.2.0", + "clone-stats": "^0.0.1" + } + } + } + }, + "decompress-targz": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-3.1.0.tgz", + "integrity": "sha1-ssE9+YFmJomRtxXWRH9kLpaW9aA=", + "dev": true, + "optional": true, + "requires": { + "is-gzip": "^1.0.0", + "object-assign": "^2.0.0", + "strip-dirs": "^1.0.0", + "tar-stream": "^1.1.1", + "through2": "^0.6.1", + "vinyl": "^0.4.3" + }, + "dependencies": { + "clone": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/clone/-/clone-0.2.0.tgz", + "integrity": "sha1-xhJqkK1Pctv1rNskPMN3JP6T/B8=", + "dev": true, + "optional": true + }, + "object-assign": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-2.1.1.tgz", + "integrity": "sha1-Q8NuXVaf+OSBbE76i+AtJpZ8GKo=", + "dev": true, + "optional": true + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "through2": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", + "dev": true, + "optional": true, + "requires": { + "readable-stream": ">=1.0.33-1 <1.1.0-0", + "xtend": ">=4.0.0 <4.1.0-0" + } + }, + "vinyl": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.4.6.tgz", + "integrity": "sha1-LzVsh6VQolVGHza76ypbqL94SEc=", + "dev": true, + "optional": true, + "requires": { + "clone": "^0.2.0", + "clone-stats": "^0.0.1" + } + } + } + }, + "decompress-unzip": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-3.4.0.tgz", + "integrity": "sha1-YUdbQVIGa74/7hL51inRX+ZHjus=", + "dev": true, + "optional": true, + "requires": { + "is-zip": "^1.0.0", + "read-all-stream": "^3.0.0", + "stat-mode": "^0.2.0", + "strip-dirs": "^1.0.0", + "through2": "^2.0.0", + "vinyl": "^1.0.0", + "yauzl": "^2.2.1" + } + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "dev": true, + "requires": { + "clone": "^1.0.2" + } + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "defined": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", + "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=", + "dev": true + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "dev": true + }, + "deprecated": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/deprecated/-/deprecated-0.0.1.tgz", + "integrity": "sha1-+cmvVGSvoeepcUWKi97yqpTVuxk=", + "dev": true + }, + "deps-sort": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/deps-sort/-/deps-sort-2.0.0.tgz", + "integrity": "sha1-CRckkC6EZYJg65EHSMzNGvbiH7U=", + "dev": true, + "requires": { + "JSONStream": "^1.0.3", + "shasum": "^1.0.0", + "subarg": "^1.0.0", + "through2": "^2.0.0" + } + }, + "des.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", + "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", + "dev": true + }, + "detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", + "dev": true + }, + "detect-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", + "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", + "dev": true, + "requires": { + "repeating": "^2.0.0" + } + }, + "detective": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/detective/-/detective-4.7.1.tgz", + "integrity": "sha512-H6PmeeUcZloWtdt4DAkFyzFL94arpHr3NOwwmVILFiy+9Qd4JTxxXrzfyGk/lmct2qVGBwTSwSXagqu2BxmWig==", + "dev": true, + "requires": { + "acorn": "^5.2.1", + "defined": "^1.0.0" + }, + "dependencies": { + "acorn": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", + "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==", + "dev": true + } + } + }, + "diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + } + }, + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "domain-browser": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.1.7.tgz", + "integrity": "sha1-hnqksJP6oF8d4IwG9NeyH9+GmLw=", + "dev": true + }, + "dot-case": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-1.1.2.tgz", + "integrity": "sha1-HnOCaQDeKNbeVIC8HeMdCEKwa+w=", + "dev": true, + "requires": { + "sentence-case": "^1.1.2" + } + }, + "dot-parts": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dot-parts/-/dot-parts-1.0.1.tgz", + "integrity": "sha1-iEvXvPwwgv+tL+XbU+SU2PPgdD8=", + "dev": true + }, + "dot-prop": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", + "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", + "dev": true, + "requires": { + "is-obj": "^1.0.0" + } + }, + "download": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/download/-/download-4.4.3.tgz", + "integrity": "sha1-qlX9rTktldS2jowr4D4MKqIbqaw=", + "dev": true, + "optional": true, + "requires": { + "caw": "^1.0.1", + "concat-stream": "^1.4.7", + "each-async": "^1.0.0", + "filenamify": "^1.0.1", + "got": "^5.0.0", + "gulp-decompress": "^1.2.0", + "gulp-rename": "^1.2.0", + "is-url": "^1.2.0", + "object-assign": "^4.0.1", + "read-all-stream": "^3.0.0", + "readable-stream": "^2.0.2", + "stream-combiner2": "^1.1.1", + "vinyl": "^1.0.0", + "vinyl-fs": "^2.2.0", + "ware": "^1.2.0" + } + }, + "duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", + "dev": true, + "requires": { + "readable-stream": "^2.0.2" + } + }, + "duplexer3": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", + "dev": true + }, + "duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dev": true, + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "each-async": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/each-async/-/each-async-1.1.1.tgz", + "integrity": "sha1-3uUim98KtrogEqOV4bhpq/iBNHM=", + "dev": true, + "requires": { + "onetime": "^1.0.0", + "set-immediate-shim": "^1.0.0" + }, + "dependencies": { + "onetime": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", + "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", + "dev": true + } + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "dev": true, + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "dev": true + }, + "electron-to-chromium": { + "version": "1.3.209", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.209.tgz", + "integrity": "sha512-KxRvLp5jUapyKIcMaecwgmUpJEsJKuHn0DJJPZjZh2valqYlzdmGvaE/nTAqwKqQwf0jIKv7Go4FYHu9wKWzOg==", + "dev": true + }, + "elliptic": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.0.tgz", + "integrity": "sha512-eFOJTMyCYb7xtE/caJ6JJu+bhi67WCYNbkGSknu20pmM8Ke/bqOfdnZWxyoGN26JgfxTbXrsCkEw4KheCT/KGg==", + "dev": true, + "requires": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" + } + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", + "dev": true + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "dev": true + }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "enhanced-resolve": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz", + "integrity": "sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "memory-fs": "^0.4.0", + "tapable": "^1.0.0" + } + }, + "errno": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", + "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", + "dev": true, + "requires": { + "prr": "~1.0.1" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es6-promise": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.1.2.tgz", + "integrity": "sha1-eV4lzrR/e6uyY9FRr77dktGOagc=" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "escodegen": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.1.0.tgz", + "integrity": "sha1-xmOSP24gqtSNDA+knzHG1PSTYM8=", + "dev": true, + "requires": { + "esprima": "~1.0.4", + "estraverse": "~1.5.0", + "esutils": "~1.0.0", + "source-map": "~0.1.30" + }, + "dependencies": { + "esprima": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", + "integrity": "sha1-n1V+CPw7TSbs6d00+Pv0drYlha0=", + "dev": true + }, + "esutils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-1.0.0.tgz", + "integrity": "sha1-gVHTWOIMisx/t0XnRywAJf5JZXA=", + "dev": true + }, + "source-map": { + "version": "0.1.43", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", + "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", + "dev": true, + "optional": true, + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, + "eslint": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-4.18.2.tgz", + "integrity": "sha512-qy4i3wODqKMYfz9LUI8N2qYDkHkoieTbiHpMrYUI/WbjhXJQr7lI4VngixTgaG+yHX+NBCv7nW4hA0ShbvaNKw==", + "dev": true, + "requires": { + "ajv": "^5.3.0", + "babel-code-frame": "^6.22.0", + "chalk": "^2.1.0", + "concat-stream": "^1.6.0", + "cross-spawn": "^5.1.0", + "debug": "^3.1.0", + "doctrine": "^2.1.0", + "eslint-scope": "^3.7.1", + "eslint-visitor-keys": "^1.0.0", + "espree": "^3.5.2", + "esquery": "^1.0.0", + "esutils": "^2.0.2", + "file-entry-cache": "^2.0.0", + "functional-red-black-tree": "^1.0.1", + "glob": "^7.1.2", + "globals": "^11.0.1", + "ignore": "^3.3.3", + "imurmurhash": "^0.1.4", + "inquirer": "^3.0.6", + "is-resolvable": "^1.0.0", + "js-yaml": "^3.9.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.4", + "minimatch": "^3.0.2", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.2", + "path-is-inside": "^1.0.2", + "pluralize": "^7.0.0", + "progress": "^2.0.0", + "require-uncached": "^1.0.3", + "semver": "^5.3.0", + "strip-ansi": "^4.0.0", + "strip-json-comments": "~2.0.1", + "table": "4.0.2", + "text-table": "~0.2.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "eslint-config-semistandard": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-semistandard/-/eslint-config-semistandard-11.0.0.tgz", + "integrity": "sha1-RO73z9/Uchnjp7gbkbVA6IC7JhU=", + "dev": true + }, + "eslint-config-standard": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-10.2.1.tgz", + "integrity": "sha1-wGHk0GbzedwXzVYsZOgZtN1FRZE=", + "dev": true + }, + "eslint-import-resolver-node": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz", + "integrity": "sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q==", + "dev": true, + "requires": { + "debug": "^2.6.9", + "resolve": "^1.5.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "eslint-module-utils": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.4.1.tgz", + "integrity": "sha512-H6DOj+ejw7Tesdgbfs4jeS4YMFrT8uI8xwd1gtQqXssaR0EQ26L+2O/w6wkYFy2MymON0fTwHmXBvvfLNZVZEw==", + "dev": true, + "requires": { + "debug": "^2.6.8", + "pkg-dir": "^2.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "eslint-plugin-import": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.7.0.tgz", + "integrity": "sha512-HGYmpU9f/zJaQiKNQOVfHUh2oLWW3STBrCgH0sHTX1xtsxYlH1zjLh8FlQGEIdZSdTbUMaV36WaZ6ImXkenGxQ==", + "dev": true, + "requires": { + "builtin-modules": "^1.1.1", + "contains-path": "^0.1.0", + "debug": "^2.6.8", + "doctrine": "1.5.0", + "eslint-import-resolver-node": "^0.3.1", + "eslint-module-utils": "^2.1.1", + "has": "^1.0.1", + "lodash.cond": "^4.3.0", + "minimatch": "^3.0.3", + "read-pkg-up": "^2.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "doctrine": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", + "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "isarray": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "eslint-plugin-node": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-5.2.1.tgz", + "integrity": "sha512-xhPXrh0Vl/b7870uEbaumb2Q+LxaEcOQ3kS1jtIXanBAwpMre1l5q/l2l/hESYJGEFKuI78bp6Uw50hlpr7B+g==", + "dev": true, + "requires": { + "ignore": "^3.3.6", + "minimatch": "^3.0.4", + "resolve": "^1.3.3", + "semver": "5.3.0" + }, + "dependencies": { + "semver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", + "dev": true + } + } + }, + "eslint-plugin-promise": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-3.5.0.tgz", + "integrity": "sha1-ePu2/+BHIBYnVp6FpsU3OvKmj8o=", + "dev": true + }, + "eslint-plugin-standard": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-standard/-/eslint-plugin-standard-3.0.1.tgz", + "integrity": "sha1-NNDJFbRe3G8BA5PH7vOCOwhWXPI=", + "dev": true + }, + "eslint-scope": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.3.tgz", + "integrity": "sha512-W+B0SvF4gamyCTmUc+uITPY0989iXVfKvhwtmJocTaYoc/3khEHmEmvfY/Gn9HA9VV75jrQECsHizkNw1b68FA==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + }, + "dependencies": { + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + } + } + }, + "eslint-visitor-keys": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz", + "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==", + "dev": true + }, + "espree": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.4.tgz", + "integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==", + "dev": true, + "requires": { + "acorn": "^5.5.0", + "acorn-jsx": "^3.0.0" + }, + "dependencies": { + "acorn": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", + "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==", + "dev": true + } + } + }, + "esprima-fb": { + "version": "3001.1.0-dev-harmony-fb", + "resolved": "https://registry.npmjs.org/esprima-fb/-/esprima-fb-3001.0001.0000-dev-harmony-fb.tgz", + "integrity": "sha1-t303q8046gt3Qmu4vCkizmtCZBE=", + "dev": true + }, + "esquery": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", + "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", + "dev": true, + "requires": { + "estraverse": "^4.0.0" + }, + "dependencies": { + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "^4.1.0" + }, + "dependencies": { + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + } + } + }, + "estraverse": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.5.1.tgz", + "integrity": "sha1-hno+jlip+EYYr7bC3bzZFrfLr3E=", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "dev": true + }, + "eventemitter2": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", + "integrity": "sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas=", + "dev": true + }, + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", + "dev": true + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dev": true, + "requires": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "exec-buffer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/exec-buffer/-/exec-buffer-2.0.1.tgz", + "integrity": "sha1-ACijG+CxRgth0HX5avRYO54zXqA=", + "dev": true, + "optional": true, + "requires": { + "rimraf": "^2.2.6", + "tempfile": "^1.0.0" + } + }, + "exec-series": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/exec-series/-/exec-series-1.0.3.tgz", + "integrity": "sha1-bSV6m+rEgqhyx3g7yGFYOfx3FDo=", + "dev": true, + "optional": true, + "requires": { + "async-each-series": "^1.1.0", + "object-assign": "^4.1.0" + } + }, + "execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "dev": true, + "requires": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "executable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/executable/-/executable-1.1.0.tgz", + "integrity": "sha1-h3mA6REvM5EGbaNyZd562ENKtNk=", + "dev": true, + "optional": true, + "requires": { + "meow": "^3.1.0" + } + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "dev": true + }, + "exit-hook": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", + "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=", + "dev": true + }, + "exorcist": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/exorcist/-/exorcist-0.4.0.tgz", + "integrity": "sha1-EjD/3t2SSPQvvM+LSkTUyrKePGQ=", + "dev": true, + "requires": { + "minimist": "0.0.5", + "mold-source-map": "~0.4.0", + "nave": "~0.5.1" + }, + "dependencies": { + "minimist": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.5.tgz", + "integrity": "sha1-16oye87PUY+RBqxrjwA/o7zqhWY=", + "dev": true + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "expand-range": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", + "dev": true, + "requires": { + "fill-range": "^2.1.0" + }, + "dependencies": { + "fill-range": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", + "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", + "dev": true, + "requires": { + "is-number": "^2.1.0", + "isobject": "^2.0.0", + "randomatic": "^3.0.0", + "repeat-element": "^1.1.2", + "repeat-string": "^1.5.2" + } + }, + "is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", + "dev": true, + "requires": { + "homedir-polyfill": "^1.0.1" + } + }, + "exposify": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/exposify/-/exposify-0.4.3.tgz", + "integrity": "sha1-GWPrNMSJ+L+6At/Sf8z7wRc4TJ4=", + "dev": true, + "requires": { + "globo": "~1.0.0", + "has-require": "~1.1.0", + "map-obj": "~1.0.1", + "replace-requires": "~1.0.1", + "through2": "~0.4.0", + "transformify": "~0.1.1" + }, + "dependencies": { + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "through2": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.4.2.tgz", + "integrity": "sha1-2/WGYDEVHsg1K7bE22SiKSqEC5s=", + "dev": true, + "requires": { + "readable-stream": "~1.0.17", + "xtend": "~2.1.1" + } + }, + "xtend": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", + "integrity": "sha1-bv7MKk2tjmlixJAbM3znuoe10os=", + "dev": true, + "requires": { + "object-keys": "~0.4.0" + } + } + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "external-editor": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", + "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", + "dev": true, + "requires": { + "chardet": "^0.4.0", + "iconv-lite": "^0.4.17", + "tmp": "^0.0.33" + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "extract-zip": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.7.tgz", + "integrity": "sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k=", + "dev": true, + "requires": { + "concat-stream": "1.6.2", + "debug": "2.6.9", + "mkdirp": "0.5.1", + "yauzl": "2.4.1" + }, + "dependencies": { + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "fd-slicer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz", + "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=", + "dev": true, + "requires": { + "pend": "~1.2.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "yauzl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz", + "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=", + "dev": true, + "requires": { + "fd-slicer": "~1.0.1" + } + } + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, + "fancy-log": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.3.tgz", + "integrity": "sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw==", + "dev": true, + "requires": { + "ansi-gray": "^0.1.1", + "color-support": "^1.1.3", + "parse-node-version": "^1.0.0", + "time-stamp": "^1.0.0" + } + }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "fastly": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/fastly/-/fastly-1.2.1.tgz", + "integrity": "sha1-3BqYn+JtYPZNbxB7Qa8B0SLvF/o=", + "dev": true, + "requires": { + "request": "~2.12.0" + }, + "dependencies": { + "request": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.12.0.tgz", + "integrity": "sha1-EfRvILPQ9ISMY4OZHIB5CvFsjkg=", + "dev": true, + "requires": { + "form-data": "~0.0.3", + "mime": "~1.2.7" + }, + "dependencies": { + "form-data": { + "version": "0.0.3", + "bundled": true, + "dev": true, + "requires": { + "async": "~0.1.9", + "combined-stream": "0.0.3", + "mime": "~1.2.2" + }, + "dependencies": { + "async": { + "version": "0.1.9", + "bundled": true, + "dev": true + }, + "combined-stream": { + "version": "0.0.3", + "bundled": true, + "dev": true, + "requires": { + "delayed-stream": "0.0.5" + }, + "dependencies": { + "delayed-stream": { + "version": "0.0.5", + "bundled": true, + "dev": true + } + } + } + } + }, + "mime": { + "version": "1.2.7", + "bundled": true, + "dev": true + } + } + } + } + }, + "faye-websocket": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.4.4.tgz", + "integrity": "sha1-wUxbO/FNdBf/v9mQwKdJXNnzN7w=", + "dev": true + }, + "fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "dev": true, + "optional": true, + "requires": { + "pend": "~1.2.0" + } + }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-entry-cache": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", + "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", + "dev": true, + "requires": { + "flat-cache": "^1.2.1", + "object-assign": "^4.0.1" + } + }, + "file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha1-JXoHg4TR24CHvESdEH1SpSZyuek=", + "dev": true + }, + "filename-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", + "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", + "dev": true + }, + "filename-reserved-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-1.0.0.tgz", + "integrity": "sha1-5hz4BfDeHJhFZ9A4bcXfUO5a9+Q=", + "dev": true, + "optional": true + }, + "filenamify": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-1.2.1.tgz", + "integrity": "sha1-qfL/0RxQO+0wABUCknI3jx8TZaU=", + "dev": true, + "optional": true, + "requires": { + "filename-reserved-regex": "^1.0.0", + "strip-outer": "^1.0.0", + "trim-repeated": "^1.0.0" + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "find-cache-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-1.0.0.tgz", + "integrity": "sha1-kojj6ePMN0hxfTnq3hfPcfww7m8=", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^1.0.0", + "pkg-dir": "^2.0.0" + } + }, + "find-index": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/find-index/-/find-index-0.1.1.tgz", + "integrity": "sha1-Z101iyyjiS15Whq0cjL4tuLg3eQ=", + "dev": true + }, + "find-parent-dir": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/find-parent-dir/-/find-parent-dir-0.3.0.tgz", + "integrity": "sha1-M8RLQpqysvBkYpnF+fcY83b/jVQ=", + "dev": true + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "find-versions": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-1.2.1.tgz", + "integrity": "sha1-y96fEuOFdaCvG+G5osXV/Y8Ya2I=", + "dev": true, + "optional": true, + "requires": { + "array-uniq": "^1.0.0", + "get-stdin": "^4.0.1", + "meow": "^3.5.0", + "semver-regex": "^1.0.0" + } + }, + "findup-sync": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.1.3.tgz", + "integrity": "sha1-fz56l7gjksZTvwZYm9hRkOk8NoM=", + "dev": true, + "requires": { + "glob": "~3.2.9", + "lodash": "~2.4.1" + }, + "dependencies": { + "glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", + "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", + "dev": true, + "requires": { + "inherits": "2", + "minimatch": "0.3" + } + }, + "lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true + }, + "lru-cache": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", + "dev": true + }, + "minimatch": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", + "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", + "dev": true, + "requires": { + "lru-cache": "2", + "sigmund": "~1.0.0" + } + } + } + }, + "first-chunk-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/first-chunk-stream/-/first-chunk-stream-1.0.0.tgz", + "integrity": "sha1-Wb+1DNkF9g18OUzT2ayqtOatk04=", + "dev": true + }, + "flagged-respawn": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-0.3.2.tgz", + "integrity": "sha1-/xke3c1wiKZ1smEP/8l2vpuAdLU=", + "dev": true + }, + "flat-cache": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.4.tgz", + "integrity": "sha512-VwyB3Lkgacfik2vhqR4uv2rvebqmDvFu4jlN/C1RzWoJEo8I7z4Q404oiqYCkq41mni8EzQnm95emU9seckwtg==", + "dev": true, + "requires": { + "circular-json": "^0.3.1", + "graceful-fs": "^4.1.2", + "rimraf": "~2.6.2", + "write": "^0.2.1" + } + }, + "flush-write-stream": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", + "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "dev": true, + "requires": { + "for-in": "^1.0.1" + } + }, + "foreach": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", + "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=", + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "requires": { + "map-cache": "^0.2.2" + } + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "dev": true + }, + "from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "optional": true + }, + "fs-extra": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-1.0.0.tgz", + "integrity": "sha1-zTzl9+fLYUWIP8rjGR6Yd/hYeVA=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^2.1.0", + "klaw": "^1.0.0" + } + }, + "fs-write-stream-atomic": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", + "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "iferr": "^0.1.5", + "imurmurhash": "^0.1.4", + "readable-stream": "1 || 2" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", + "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", + "dev": true, + "optional": true, + "requires": { + "nan": "^2.12.1", + "node-pre-gyp": "^0.12.0" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "debug": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ms": "^2.1.1" + } + }, + "deep-extend": { + "version": "0.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.24", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true, + "optional": true + }, + "minipass": { + "version": "2.3.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.2.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "needle": { + "version": "2.3.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "debug": "^4.1.0", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.12.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true + }, + "npm-packlist": { + "version": "1.4.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.6.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "dev": true, + "optional": true + }, + "semver": { + "version": "5.7.0", + "bundled": true, + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "tar": { + "version": "4.4.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "wide-align": { + "version": "1.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "yallist": { + "version": "3.0.3", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "dev": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + } + } + }, + "gaze": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-0.4.3.tgz", + "integrity": "sha1-5Tj0/15P5kj0c6l+HrslPS3hJ7U=", + "dev": true, + "requires": { + "globule": "~0.1.0" + } + }, + "get-assigned-identifiers": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz", + "integrity": "sha512-mBBwmeGTrxEMO4pMaaf/uUEFHnYtwr8FTe8Y/mer4rcV/bye0qGm6pw1bGZFGStxC5O76c5ZAVBGnqHmOaJpdQ==", + "dev": true + }, + "get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", + "dev": true + }, + "get-proxy": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-proxy/-/get-proxy-1.1.0.tgz", + "integrity": "sha1-iUhUSRvFkbDxR9euVw9cZ4tyVus=", + "dev": true, + "optional": true, + "requires": { + "rc": "^1.1.2" + } + }, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", + "dev": true + }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true + }, + "getobject": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz", + "integrity": "sha1-BHpEl4n6Fg0Bj1SG7ZEyC27HiFw=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "gifsicle": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/gifsicle/-/gifsicle-3.0.4.tgz", + "integrity": "sha1-9Fy17RAWW2ZdySng6TKLbIId+js=", + "dev": true, + "optional": true, + "requires": { + "bin-build": "^2.0.0", + "bin-wrapper": "^3.0.0", + "logalot": "^2.0.0" + } + }, + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "dev": true, + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "dev": true, + "requires": { + "glob-parent": "^2.0.0", + "is-glob": "^2.0.0" + }, + "dependencies": { + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "dev": true, + "requires": { + "is-glob": "^2.0.0" + } + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "^1.0.0" + } + } + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "glob-stream": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-5.3.5.tgz", + "integrity": "sha1-pVZlqajM3EGRWofHAeMtTgFvrSI=", + "dev": true, + "requires": { + "extend": "^3.0.0", + "glob": "^5.0.3", + "glob-parent": "^3.0.0", + "micromatch": "^2.3.7", + "ordered-read-streams": "^0.3.0", + "through2": "^0.6.0", + "to-absolute-glob": "^0.1.1", + "unique-stream": "^2.0.2" + }, + "dependencies": { + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "dev": true, + "requires": { + "arr-flatten": "^1.0.1" + } + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "dev": true + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "dev": true, + "requires": { + "expand-range": "^1.8.1", + "preserve": "^0.2.0", + "repeat-element": "^1.1.2" + } + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "dev": true, + "requires": { + "is-posix-bracket": "^0.1.0" + } + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "dev": true, + "requires": { + "is-extglob": "^1.0.0" + } + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "^1.0.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "dev": true, + "requires": { + "arr-diff": "^2.0.0", + "array-unique": "^0.2.1", + "braces": "^1.8.2", + "expand-brackets": "^0.1.4", + "extglob": "^0.3.1", + "filename-regex": "^2.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.1", + "kind-of": "^3.0.2", + "normalize-path": "^2.0.1", + "object.omit": "^2.0.0", + "parse-glob": "^3.0.4", + "regex-cache": "^0.4.2" + } + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "through2": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", + "dev": true, + "requires": { + "readable-stream": ">=1.0.33-1 <1.1.0-0", + "xtend": ">=4.0.0 <4.1.0-0" + } + } + } + }, + "glob-watcher": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-0.0.6.tgz", + "integrity": "sha1-uVtKjfdLOcgymLDAXJeLTZo7cQs=", + "dev": true, + "requires": { + "gaze": "^0.5.1" + }, + "dependencies": { + "gaze": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-0.5.2.tgz", + "integrity": "sha1-QLcJU30k0dRXZ9takIaJ3+aaxE8=", + "dev": true, + "requires": { + "globule": "~0.1.0" + } + } + } + }, + "glob2base": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/glob2base/-/glob2base-0.0.12.tgz", + "integrity": "sha1-nUGbPijxLoOjYhZKJ3BVkiycDVY=", + "dev": true, + "requires": { + "find-index": "^0.1.1" + } + }, + "global-dirs": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", + "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", + "dev": true, + "requires": { + "ini": "^1.3.4" + } + }, + "global-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", + "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", + "dev": true, + "requires": { + "global-prefix": "^3.0.0" + }, + "dependencies": { + "global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "dev": true, + "requires": { + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" + } + } + } + }, + "global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", + "dev": true, + "requires": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + } + }, + "globals": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", + "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", + "dev": true + }, + "globo": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/globo/-/globo-1.0.2.tgz", + "integrity": "sha1-aPdc4qBFRA06cEExeG+I1CBfSb0=", + "dev": true, + "requires": { + "accessory": "~1.0.0", + "is-defined": "~1.0.0", + "ternary": "~1.0.0" + } + }, + "globule": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/globule/-/globule-0.1.0.tgz", + "integrity": "sha1-2cjt3h2nnRJaFRt5UzuXhnY0auU=", + "dev": true, + "requires": { + "glob": "~3.1.21", + "lodash": "~1.0.1", + "minimatch": "~0.2.11" + }, + "dependencies": { + "glob": { + "version": "3.1.21", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz", + "integrity": "sha1-0p4KBV3qUTj00H7UDomC6DwgZs0=", + "dev": true, + "requires": { + "graceful-fs": "~1.2.0", + "inherits": "1", + "minimatch": "~0.2.11" + } + }, + "graceful-fs": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz", + "integrity": "sha1-FaSAaldUfLLS2/J/QuiajDRRs2Q=", + "dev": true + }, + "inherits": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz", + "integrity": "sha1-ykMJ2t7mtUzAuNJH6NfHoJdb3Js=", + "dev": true + }, + "lodash": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-1.0.2.tgz", + "integrity": "sha1-j1dWDIO1n8JwvT1WG2kAQ0MOJVE=", + "dev": true + }, + "lru-cache": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", + "dev": true + }, + "minimatch": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", + "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", + "dev": true, + "requires": { + "lru-cache": "2", + "sigmund": "~1.0.0" + } + } + } + }, + "glogg": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.2.tgz", + "integrity": "sha512-5mwUoSuBk44Y4EshyiqcH95ZntbDdTQqA3QYSrxmzj28Ai0vXBGMH1ApSANH14j2sIRtqCEyg6PfsuP7ElOEDA==", + "dev": true, + "requires": { + "sparkles": "^1.0.0" + } + }, + "got": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/got/-/got-5.7.1.tgz", + "integrity": "sha1-X4FjWmHkplifGAVp6k44FoClHzU=", + "dev": true, + "optional": true, + "requires": { + "create-error-class": "^3.0.1", + "duplexer2": "^0.1.4", + "is-redirect": "^1.0.0", + "is-retry-allowed": "^1.0.0", + "is-stream": "^1.0.0", + "lowercase-keys": "^1.0.0", + "node-status-codes": "^1.0.0", + "object-assign": "^4.0.1", + "parse-json": "^2.1.0", + "pinkie-promise": "^2.0.0", + "read-all-stream": "^3.0.0", + "readable-stream": "^2.0.5", + "timed-out": "^3.0.0", + "unzip-response": "^1.0.2", + "url-parse-lax": "^1.0.0" + } + }, + "graceful-fs": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.0.tgz", + "integrity": "sha512-jpSvDPV4Cq/bgtpndIWbI5hmYxhQGHPC4d4cqBPb4DLniCfhJokdXhwhaDuLBGLQdvvRum/UiX6ECVIPvDXqdg==", + "dev": true + }, + "graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", + "dev": true, + "optional": true + }, + "grunt": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/grunt/-/grunt-0.4.5.tgz", + "integrity": "sha1-VpN81RlDJK3/bSB2MYMqnWuk5/A=", + "dev": true, + "requires": { + "async": "~0.1.22", + "coffee-script": "~1.3.3", + "colors": "~0.6.2", + "dateformat": "1.0.2-1.2.3", + "eventemitter2": "~0.4.13", + "exit": "~0.1.1", + "findup-sync": "~0.1.2", + "getobject": "~0.1.0", + "glob": "~3.1.21", + "grunt-legacy-log": "~0.1.0", + "grunt-legacy-util": "~0.2.0", + "hooker": "~0.2.3", + "iconv-lite": "~0.2.11", + "js-yaml": "~2.0.5", + "lodash": "~0.9.2", + "minimatch": "~0.2.12", + "nopt": "~1.0.10", + "rimraf": "~2.2.8", + "underscore.string": "~2.2.1", + "which": "~1.0.5" + }, + "dependencies": { + "argparse": { + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-0.1.16.tgz", + "integrity": "sha1-z9AeD7uj1srtBJ+9dY1A9lGW9Xw=", + "dev": true, + "requires": { + "underscore": "~1.7.0", + "underscore.string": "~2.4.0" + }, + "dependencies": { + "underscore.string": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.4.0.tgz", + "integrity": "sha1-jN2PusTi0uoefi6Al8QvRCKA+Fs=", + "dev": true + } + } + }, + "esprima": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", + "integrity": "sha1-n1V+CPw7TSbs6d00+Pv0drYlha0=", + "dev": true + }, + "glob": { + "version": "3.1.21", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz", + "integrity": "sha1-0p4KBV3qUTj00H7UDomC6DwgZs0=", + "dev": true, + "requires": { + "graceful-fs": "~1.2.0", + "inherits": "1", + "minimatch": "~0.2.11" + } + }, + "graceful-fs": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz", + "integrity": "sha1-FaSAaldUfLLS2/J/QuiajDRRs2Q=", + "dev": true + }, + "iconv-lite": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz", + "integrity": "sha1-HOYKOleGSiktEyH/RgnKS7llrcg=", + "dev": true + }, + "inherits": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz", + "integrity": "sha1-ykMJ2t7mtUzAuNJH6NfHoJdb3Js=", + "dev": true + }, + "js-yaml": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-2.0.5.tgz", + "integrity": "sha1-olrmUJmZ6X3yeMZxnaEb0Gh3Q6g=", + "dev": true, + "requires": { + "argparse": "~ 0.1.11", + "esprima": "~ 1.0.2" + } + }, + "lodash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz", + "integrity": "sha1-jzSZxSRdNG1oLlsNO0B2fgnxqSw=", + "dev": true + }, + "lru-cache": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", + "dev": true + }, + "minimatch": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", + "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", + "dev": true, + "requires": { + "lru-cache": "2", + "sigmund": "~1.0.0" + } + }, + "rimraf": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", + "integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=", + "dev": true + }, + "underscore": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", + "integrity": "sha1-a7rwh3UA02vjTsqlhODbn+8DUgk=", + "dev": true + }, + "which": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/which/-/which-1.0.9.tgz", + "integrity": "sha1-RgwdoPgQED0DIam2M6+eV15kSG8=", + "dev": true + } + } + }, + "grunt-aws": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/grunt-aws/-/grunt-aws-0.4.2.tgz", + "integrity": "sha1-3+auyZMxxFCVlxBNCNP7W27EuiE=", + "dev": true, + "requires": { + "async": "~0.2.9", + "aws-sdk": "~1.12.0", + "lodash": "~1.3.1", + "mime": "~1.2.11" + }, + "dependencies": { + "async": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=", + "dev": true + }, + "lodash": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-1.3.1.tgz", + "integrity": "sha1-pGY7U2hriV/wdOK6UE37dqjit3A=", + "dev": true + } + } + }, + "grunt-banner": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/grunt-banner/-/grunt-banner-0.6.0.tgz", + "integrity": "sha1-P4eQIdEj+linuloLb7a+QStYhaw=", + "dev": true, + "requires": { + "chalk": "^1.1.0" + } + }, + "grunt-browserify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/grunt-browserify/-/grunt-browserify-5.0.0.tgz", + "integrity": "sha1-HTM8qYvaxEV29kbg1mgAh8v4xhU=", + "dev": true, + "requires": { + "async": "^1.5.0", + "browserify": "^13.0.0", + "glob": "^6.0.3", + "lodash": "^3.10.1", + "resolve": "^1.1.6", + "watchify": "^3.6.1" + }, + "dependencies": { + "assert": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", + "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", + "dev": true, + "requires": { + "object-assign": "^4.1.1", + "util": "0.10.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "dev": true + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "dev": true, + "requires": { + "inherits": "2.0.1" + } + } + } + }, + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, + "requires": { + "pako": "~1.0.5" + } + }, + "buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz", + "integrity": "sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==", + "dev": true, + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "detective": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz", + "integrity": "sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg==", + "dev": true, + "requires": { + "acorn-node": "^1.6.1", + "defined": "^1.0.0", + "minimist": "^1.1.1" + } + }, + "domain-browser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", + "dev": true + }, + "events": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/events/-/events-2.1.0.tgz", + "integrity": "sha512-3Zmiobend8P9DjmKAty0Era4jV8oJ0yGYe2nJJAxgymF9+N8F2m0hhZiMoWtcfepExzNKZumFU3ksdQbInGWCg==", + "dev": true + }, + "glob": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", + "dev": true, + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", + "dev": true + }, + "lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=", + "dev": true + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "module-deps": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-6.2.1.tgz", + "integrity": "sha512-UnEn6Ah36Tu4jFiBbJVUtt0h+iXqxpLqDvPS8nllbw5RZFmNJ1+Mz5BjYnM9ieH80zyxHkARGLnMIHlPK5bu6A==", + "dev": true, + "requires": { + "JSONStream": "^1.0.3", + "browser-resolve": "^1.7.0", + "cached-path-relative": "^1.0.2", + "concat-stream": "~1.6.0", + "defined": "^1.0.0", + "detective": "^5.0.2", + "duplexer2": "^0.1.2", + "inherits": "^2.0.1", + "parents": "^1.0.0", + "readable-stream": "^2.0.2", + "resolve": "^1.4.0", + "stream-combiner2": "^1.1.1", + "subarg": "^1.0.0", + "through2": "^2.0.0", + "xtend": "^4.0.0" + } + }, + "os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", + "dev": true + }, + "pako": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz", + "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==", + "dev": true + }, + "string_decoder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz", + "integrity": "sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "vm-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.0.tgz", + "integrity": "sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw==", + "dev": true + }, + "watchify": { + "version": "3.11.1", + "resolved": "https://registry.npmjs.org/watchify/-/watchify-3.11.1.tgz", + "integrity": "sha512-WwnUClyFNRMB2NIiHgJU9RQPQNqVeFk7OmZaWf5dC5EnNa0Mgr7imBydbaJ7tGTuPM2hz1Cb4uiBvK9NVxMfog==", + "dev": true, + "requires": { + "anymatch": "^2.0.0", + "browserify": "^16.1.0", + "chokidar": "^2.1.1", + "defined": "^1.0.0", + "outpipe": "^1.1.0", + "through2": "^2.0.0", + "xtend": "^4.0.0" + }, + "dependencies": { + "browserify": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/browserify/-/browserify-16.3.0.tgz", + "integrity": "sha512-BWaaD7alyGZVEBBwSTYx4iJF5DswIGzK17o8ai9w4iKRbYpk3EOiprRHMRRA8DCZFmFeOdx7A385w2XdFvxWmg==", + "dev": true, + "requires": { + "JSONStream": "^1.0.3", + "assert": "^1.4.0", + "browser-pack": "^6.0.1", + "browser-resolve": "^1.11.0", + "browserify-zlib": "~0.2.0", + "buffer": "^5.0.2", + "cached-path-relative": "^1.0.0", + "concat-stream": "^1.6.0", + "console-browserify": "^1.1.0", + "constants-browserify": "~1.0.0", + "crypto-browserify": "^3.0.0", + "defined": "^1.0.0", + "deps-sort": "^2.0.0", + "domain-browser": "^1.2.0", + "duplexer2": "~0.1.2", + "events": "^2.0.0", + "glob": "^7.1.0", + "has": "^1.0.0", + "htmlescape": "^1.1.0", + "https-browserify": "^1.0.0", + "inherits": "~2.0.1", + "insert-module-globals": "^7.0.0", + "labeled-stream-splicer": "^2.0.0", + "mkdirp": "^0.5.0", + "module-deps": "^6.0.0", + "os-browserify": "~0.3.0", + "parents": "^1.0.1", + "path-browserify": "~0.0.0", + "process": "~0.11.0", + "punycode": "^1.3.2", + "querystring-es3": "~0.2.0", + "read-only-stream": "^2.0.0", + "readable-stream": "^2.0.2", + "resolve": "^1.1.4", + "shasum": "^1.0.0", + "shell-quote": "^1.6.1", + "stream-browserify": "^2.0.0", + "stream-http": "^2.0.0", + "string_decoder": "^1.1.1", + "subarg": "^1.0.0", + "syntax-error": "^1.1.1", + "through2": "^2.0.0", + "timers-browserify": "^1.0.1", + "tty-browserify": "0.0.1", + "url": "~0.11.0", + "util": "~0.10.1", + "vm-browserify": "^1.0.0", + "xtend": "^4.0.0" + } + }, + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + } + } + }, + "grunt-contrib-clean": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-clean/-/grunt-contrib-clean-0.5.0.tgz", + "integrity": "sha1-9T397ghJsce0Dp67umn0jExgecU=", + "dev": true, + "requires": { + "rimraf": "~2.2.1" + }, + "dependencies": { + "rimraf": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", + "integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=", + "dev": true + } + } + }, + "grunt-contrib-concat": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-concat/-/grunt-contrib-concat-0.3.0.tgz", + "integrity": "sha1-SPoNQzbSm2U62CJaa9b4VrRIPjI=", + "dev": true + }, + "grunt-contrib-connect": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/grunt-contrib-connect/-/grunt-contrib-connect-0.11.2.tgz", + "integrity": "sha1-HAoHB9OzKNnPO0tJDrhMSV2Tau0=", + "dev": true, + "requires": { + "async": "^0.9.0", + "connect": "^3.4.0", + "connect-livereload": "^0.5.0", + "morgan": "^1.6.1", + "opn": "^1.0.0", + "portscanner": "^1.0.0", + "serve-index": "^1.7.1", + "serve-static": "^1.10.0" + }, + "dependencies": { + "async": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", + "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=", + "dev": true + } + } + }, + "grunt-contrib-copy": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-copy/-/grunt-contrib-copy-0.7.0.tgz", + "integrity": "sha1-xt5I4N9zFEmu2w8InAldvCpVBQ8=", + "dev": true, + "requires": { + "chalk": "~0.5.1" + }, + "dependencies": { + "ansi-regex": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz", + "integrity": "sha1-DY6UaWej2BQ/k+JOKYUl/BsiNfk=", + "dev": true + }, + "ansi-styles": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz", + "integrity": "sha1-6uy/Zs1waIJ2Cy9GkVgrj1XXp94=", + "dev": true + }, + "chalk": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz", + "integrity": "sha1-Zjs6ZItotV0EaQ1JFnqoN4WPIXQ=", + "dev": true, + "requires": { + "ansi-styles": "^1.1.0", + "escape-string-regexp": "^1.0.0", + "has-ansi": "^0.1.0", + "strip-ansi": "^0.3.0", + "supports-color": "^0.2.0" + } + }, + "has-ansi": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz", + "integrity": "sha1-hPJlqujA5qiKEtcCKJS3VoiUxi4=", + "dev": true, + "requires": { + "ansi-regex": "^0.2.0" + } + }, + "strip-ansi": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz", + "integrity": "sha1-JfSOoiynkYfzF0pNuHWTR7sSYiA=", + "dev": true, + "requires": { + "ansi-regex": "^0.2.1" + } + }, + "supports-color": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz", + "integrity": "sha1-2S3iaU6z9nMjlz1649i1W0wiGQo=", + "dev": true + } + } + }, + "grunt-contrib-cssmin": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-cssmin/-/grunt-contrib-cssmin-0.7.0.tgz", + "integrity": "sha1-pXNenx0mMUnkn+A1KU5CnYxnC6s=", + "dev": true, + "requires": { + "clean-css": "~2.0.0", + "grunt-lib-contrib": "~0.6.0" + } + }, + "grunt-contrib-imagemin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/grunt-contrib-imagemin/-/grunt-contrib-imagemin-1.0.1.tgz", + "integrity": "sha1-5Ho1YTN29MqpwfkERlA8rhyUTXk=", + "dev": true, + "requires": { + "async": "^1.5.2", + "chalk": "^1.0.0", + "gulp-rename": "^1.2.0", + "imagemin": "^4.0.0", + "pretty-bytes": "^3.0.1" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + } + } + }, + "grunt-contrib-jasmine": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-jasmine/-/grunt-contrib-jasmine-1.1.0.tgz", + "integrity": "sha1-9oL3dX2il3Wf4+G0xl1GcMw66kk=", + "dev": true, + "requires": { + "chalk": "^1.0.0", + "grunt-lib-phantomjs": "^1.0.0", + "jasmine-core": "~2.4.0", + "lodash": "~2.4.1", + "rimraf": "^2.1.4", + "sprintf-js": "~1.0.3" + }, + "dependencies": { + "lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true + } + } + }, + "grunt-contrib-watch": { + "version": "git://github.com/gruntjs/grunt-contrib-watch.git#b884948805940c663b1cbb91a3c28ba8afdebf78", + "from": "git://github.com/gruntjs/grunt-contrib-watch.git#b884948805940c663b1cbb91a3c28ba8afdebf78", + "dev": true, + "requires": { + "async": "~0.2.9", + "gaze": "~0.4.0", + "lodash": "~2.4.1", + "tiny-lr": "0.0.5" + }, + "dependencies": { + "async": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=", + "dev": true + }, + "lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true + } + } + }, + "grunt-eslint": { + "version": "20.1.0", + "resolved": "https://registry.npmjs.org/grunt-eslint/-/grunt-eslint-20.1.0.tgz", + "integrity": "sha512-VZlDOLrB2KKefDDcx/wR8rEEz7smDwDKVblmooa+itdt/2jWw3ee2AiZB5Ap4s4AoRY0pbHRjZ3HHwY8uKR9Rw==", + "dev": true, + "requires": { + "chalk": "^2.1.0", + "eslint": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "grunt-exorcise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/grunt-exorcise/-/grunt-exorcise-2.1.0.tgz", + "integrity": "sha1-1IWH9r+P5rPd3040tCncv0gubH0=", + "dev": true, + "requires": { + "async": "~0.2.10", + "concat-stream": "~1.4.1", + "exorcist": "^0.4.0", + "resumer": "0.0.0" + }, + "dependencies": { + "async": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=", + "dev": true + }, + "concat-stream": { + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.4.11.tgz", + "integrity": "sha512-X3JMh8+4je3U1cQpG87+f9lXHDrqcb2MVLg9L7o8b1UZ0DzhRrUpdn65ttzu10PpJPPI3MQNkis+oha6TSA9Mw==", + "dev": true, + "requires": { + "inherits": "~2.0.1", + "readable-stream": "~1.1.9", + "typedarray": "~0.0.5" + } + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + } + } + }, + "grunt-fastly": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/grunt-fastly/-/grunt-fastly-0.1.5.tgz", + "integrity": "sha1-nL8ilcE34TDzjpvFYjY7LVRb2Vs=", + "dev": true, + "requires": { + "async": "~0.2.9", + "fastly": "~1.2.1" + }, + "dependencies": { + "async": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=", + "dev": true + } + } + }, + "grunt-gitinfo": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/grunt-gitinfo/-/grunt-gitinfo-0.1.9.tgz", + "integrity": "sha512-nAyUZQrLkgHZuqepTgkA3jn9CX4kyrNcWzjENxo/nOwYjZPEWDJeaxWcYrAzB5OcBk5gYJmTA0r3XCgczxtbPg==", + "dev": true, + "requires": { + "async": "~0.9.0", + "getobject": "~0.1.0", + "lodash": "^4.17.14" + }, + "dependencies": { + "async": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", + "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=", + "dev": true + } + } + }, + "grunt-legacy-log": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-0.1.3.tgz", + "integrity": "sha1-7ClCboAwIa9ZAp+H0vnNczWgVTE=", + "dev": true, + "requires": { + "colors": "~0.6.2", + "grunt-legacy-log-utils": "~0.1.1", + "hooker": "~0.2.3", + "lodash": "~2.4.1", + "underscore.string": "~2.3.3" + }, + "dependencies": { + "lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true + }, + "underscore.string": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", + "integrity": "sha1-ccCL9rQosRM/N+ePo6Icgvcymw0=", + "dev": true + } + } + }, + "grunt-legacy-log-utils": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-0.1.1.tgz", + "integrity": "sha1-wHBrndkGThFvNvI/5OawSGcsD34=", + "dev": true, + "requires": { + "colors": "~0.6.2", + "lodash": "~2.4.1", + "underscore.string": "~2.3.3" + }, + "dependencies": { + "lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true + }, + "underscore.string": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", + "integrity": "sha1-ccCL9rQosRM/N+ePo6Icgvcymw0=", + "dev": true + } + } + }, + "grunt-legacy-util": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-0.2.0.tgz", + "integrity": "sha1-kzJIhNv343qf98Am3/RR2UqeVUs=", + "dev": true, + "requires": { + "async": "~0.1.22", + "exit": "~0.1.1", + "getobject": "~0.1.0", + "hooker": "~0.2.3", + "lodash": "~0.9.2", + "underscore.string": "~2.2.1", + "which": "~1.0.5" + }, + "dependencies": { + "lodash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz", + "integrity": "sha1-jzSZxSRdNG1oLlsNO0B2fgnxqSw=", + "dev": true + }, + "which": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/which/-/which-1.0.9.tgz", + "integrity": "sha1-RgwdoPgQED0DIam2M6+eV15kSG8=", + "dev": true + } + } + }, + "grunt-lib-contrib": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/grunt-lib-contrib/-/grunt-lib-contrib-0.6.1.tgz", + "integrity": "sha1-P1att9oG6BR5XuJBWw6+X7iQPrs=", + "dev": true, + "requires": { + "zlib-browserify": "0.0.1" + } + }, + "grunt-lib-phantomjs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/grunt-lib-phantomjs/-/grunt-lib-phantomjs-1.1.0.tgz", + "integrity": "sha1-np7c3Z/S3UDgwYHJQ3HVcqpe6tI=", + "dev": true, + "requires": { + "eventemitter2": "^0.4.9", + "phantomjs-prebuilt": "^2.1.3", + "rimraf": "^2.5.2", + "semver": "^5.1.0", + "temporary": "^0.0.8" + } + }, + "grunt-prompt": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/grunt-prompt/-/grunt-prompt-1.3.3.tgz", + "integrity": "sha1-xbQ77DqimqaWKsZhGolnEvy6Z5E=", + "dev": true, + "requires": { + "inquirer": "^0.11.0", + "lodash": "^3.10.1" + }, + "dependencies": { + "ansi-escapes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", + "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=", + "dev": true + }, + "cli-cursor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", + "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", + "dev": true, + "requires": { + "restore-cursor": "^1.0.1" + } + }, + "cli-width": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-1.1.1.tgz", + "integrity": "sha1-pNKT72frt7iNSk1CwMzwDE0eNm0=", + "dev": true + }, + "figures": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5", + "object-assign": "^4.1.0" + } + }, + "inquirer": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-0.11.4.tgz", + "integrity": "sha1-geM3ToNhvq/y2XAWIG01nQsy+k0=", + "dev": true, + "requires": { + "ansi-escapes": "^1.1.0", + "ansi-regex": "^2.0.0", + "chalk": "^1.0.0", + "cli-cursor": "^1.0.1", + "cli-width": "^1.0.1", + "figures": "^1.3.5", + "lodash": "^3.3.1", + "readline2": "^1.0.1", + "run-async": "^0.1.0", + "rx-lite": "^3.1.2", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.0", + "through": "^2.3.6" + } + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=", + "dev": true + }, + "onetime": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", + "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", + "dev": true + }, + "restore-cursor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", + "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", + "dev": true, + "requires": { + "exit-hook": "^1.0.0", + "onetime": "^1.0.0" + } + }, + "run-async": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz", + "integrity": "sha1-yK1KXhEGYeQCp9IbUw4AnyX444k=", + "dev": true, + "requires": { + "once": "^1.3.0" + } + }, + "rx-lite": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-3.1.2.tgz", + "integrity": "sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI=", + "dev": true + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + } + } + }, + "grunt-replace": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/grunt-replace/-/grunt-replace-0.6.2.tgz", + "integrity": "sha1-ZlEy2F7XbtP6KtNZfB51L9dmGL8=", + "dev": true, + "requires": { + "chalk": "~0.4.0", + "lodash": "~2.4.1" + }, + "dependencies": { + "ansi-styles": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.0.0.tgz", + "integrity": "sha1-yxAt8cVvUSPquLZ817mAJ6AnkXg=", + "dev": true + }, + "chalk": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.4.0.tgz", + "integrity": "sha1-UZmj3c0MHv4jvAjBsCewYXbgxk8=", + "dev": true, + "requires": { + "ansi-styles": "~1.0.0", + "has-color": "~0.1.0", + "strip-ansi": "~0.1.0" + } + }, + "lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true + }, + "strip-ansi": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.1.1.tgz", + "integrity": "sha1-OeipjQRNFQZgq+SmgIrPcLt7yZE=", + "dev": true + } + } + }, + "grunt-sass": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/grunt-sass/-/grunt-sass-2.0.0.tgz", + "integrity": "sha1-kHTPnXtFkuIPd4jKpye4+aoGtgo=", + "dev": true, + "requires": { + "each-async": "^1.0.0", + "node-sass": "^4.0.0", + "object-assign": "^4.0.1" + } + }, + "grunt-terser": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/grunt-terser/-/grunt-terser-1.0.0.tgz", + "integrity": "sha512-8MfNU3cVP4UWZLlIjJMUpk3NWIEmaD+CwewhDpUTiPaS49EkBiSWCmGAihqWxBKbiOC3KePPXMmB/yiaVNqW2w==", + "dev": true, + "requires": { + "terser": "^4.3.9" + } + }, + "gulp": { + "version": "3.8.10", + "resolved": "https://registry.npmjs.org/gulp/-/gulp-3.8.10.tgz", + "integrity": "sha1-v7j8FWvpeCDwKn+LOvYahmZvnjE=", + "dev": true, + "requires": { + "archy": "^1.0.0", + "chalk": "^0.5.0", + "deprecated": "^0.0.1", + "gulp-util": "^3.0.0", + "interpret": "^0.3.2", + "liftoff": "^0.13.2", + "minimist": "^1.1.0", + "orchestrator": "^0.3.0", + "pretty-hrtime": "^0.2.0", + "semver": "^4.1.0", + "tildify": "^1.0.0", + "v8flags": "^1.0.1", + "vinyl-fs": "^0.3.0" + }, + "dependencies": { + "ansi-regex": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz", + "integrity": "sha1-DY6UaWej2BQ/k+JOKYUl/BsiNfk=", + "dev": true + }, + "ansi-styles": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz", + "integrity": "sha1-6uy/Zs1waIJ2Cy9GkVgrj1XXp94=", + "dev": true + }, + "chalk": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz", + "integrity": "sha1-Zjs6ZItotV0EaQ1JFnqoN4WPIXQ=", + "dev": true, + "requires": { + "ansi-styles": "^1.1.0", + "escape-string-regexp": "^1.0.0", + "has-ansi": "^0.1.0", + "strip-ansi": "^0.3.0", + "supports-color": "^0.2.0" + } + }, + "clone": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/clone/-/clone-0.2.0.tgz", + "integrity": "sha1-xhJqkK1Pctv1rNskPMN3JP6T/B8=", + "dev": true + }, + "glob": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-4.5.3.tgz", + "integrity": "sha1-xstz0yJsHv7wTePFbQEvAzd+4V8=", + "dev": true, + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^2.0.1", + "once": "^1.3.0" + } + }, + "glob-stream": { + "version": "3.1.18", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-3.1.18.tgz", + "integrity": "sha1-kXCl8St5Awb9/lmPMT+PeVT9FDs=", + "dev": true, + "requires": { + "glob": "^4.3.1", + "glob2base": "^0.0.12", + "minimatch": "^2.0.1", + "ordered-read-streams": "^0.1.0", + "through2": "^0.6.1", + "unique-stream": "^1.0.0" + } + }, + "graceful-fs": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-3.0.11.tgz", + "integrity": "sha1-dhPHeKGv6mLyXGMKCG1/Osu92Bg=", + "dev": true, + "requires": { + "natives": "^1.1.0" + } + }, + "has-ansi": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz", + "integrity": "sha1-hPJlqujA5qiKEtcCKJS3VoiUxi4=", + "dev": true, + "requires": { + "ansi-regex": "^0.2.0" + } + }, + "minimatch": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-2.0.10.tgz", + "integrity": "sha1-jQh8OcazjAAbl/ynzm0OHoCvusc=", + "dev": true, + "requires": { + "brace-expansion": "^1.0.0" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "ordered-read-streams": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-0.1.0.tgz", + "integrity": "sha1-/VZamvjrRHO6abbtijQ1LLVS8SY=", + "dev": true + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "semver": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz", + "integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto=", + "dev": true + }, + "strip-ansi": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz", + "integrity": "sha1-JfSOoiynkYfzF0pNuHWTR7sSYiA=", + "dev": true, + "requires": { + "ansi-regex": "^0.2.1" + } + }, + "strip-bom": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-1.0.0.tgz", + "integrity": "sha1-hbiGLzhEtabV7IRnqTWYFzo295Q=", + "dev": true, + "requires": { + "first-chunk-stream": "^1.0.0", + "is-utf8": "^0.2.0" + } + }, + "supports-color": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz", + "integrity": "sha1-2S3iaU6z9nMjlz1649i1W0wiGQo=", + "dev": true + }, + "through2": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", + "dev": true, + "requires": { + "readable-stream": ">=1.0.33-1 <1.1.0-0", + "xtend": ">=4.0.0 <4.1.0-0" + } + }, + "unique-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-1.0.0.tgz", + "integrity": "sha1-1ZpKdUJ0R9mqbJHnAmP40mpLEEs=", + "dev": true + }, + "vinyl": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.4.6.tgz", + "integrity": "sha1-LzVsh6VQolVGHza76ypbqL94SEc=", + "dev": true, + "requires": { + "clone": "^0.2.0", + "clone-stats": "^0.0.1" + } + }, + "vinyl-fs": { + "version": "0.3.14", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-0.3.14.tgz", + "integrity": "sha1-mmhRzhysHBzqX+hsCTHWIMLPqeY=", + "dev": true, + "requires": { + "defaults": "^1.0.0", + "glob-stream": "^3.1.5", + "glob-watcher": "^0.0.6", + "graceful-fs": "^3.0.0", + "mkdirp": "^0.5.0", + "strip-bom": "^1.0.0", + "through2": "^0.6.1", + "vinyl": "^0.4.0" + } + } + } + }, + "gulp-decompress": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gulp-decompress/-/gulp-decompress-1.2.0.tgz", + "integrity": "sha1-jutlpeAV+O2FMsr+KEVJYGJvDcc=", + "dev": true, + "optional": true, + "requires": { + "archive-type": "^3.0.0", + "decompress": "^3.0.0", + "gulp-util": "^3.0.1", + "readable-stream": "^2.0.2" + } + }, + "gulp-iconfont": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gulp-iconfont/-/gulp-iconfont-1.0.0.tgz", + "integrity": "sha1-sVeRre98WHmeNCBSd6bsGoZv//k=", + "dev": true, + "requires": { + "gulp-svg2ttf": "1.0.0", + "gulp-svgicons2svgfont": "1.0.0", + "gulp-ttf2eot": "1.0.0", + "gulp-ttf2woff": "1.0.0", + "gulp-util": "~3.0.1", + "plexer": "0.0.3" + } + }, + "gulp-iconfont-css": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/gulp-iconfont-css/-/gulp-iconfont-css-0.0.9.tgz", + "integrity": "sha1-s/Xhl/IptpWdvwypzZQC37Ov848=", + "dev": true, + "requires": { + "consolidate": "~0.10.0", + "gulp-util": "~2.2.0", + "lodash": "~2.4.1" + }, + "dependencies": { + "ansi-regex": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz", + "integrity": "sha1-DY6UaWej2BQ/k+JOKYUl/BsiNfk=", + "dev": true + }, + "ansi-styles": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz", + "integrity": "sha1-6uy/Zs1waIJ2Cy9GkVgrj1XXp94=", + "dev": true + }, + "chalk": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz", + "integrity": "sha1-Zjs6ZItotV0EaQ1JFnqoN4WPIXQ=", + "dev": true, + "requires": { + "ansi-styles": "^1.1.0", + "escape-string-regexp": "^1.0.0", + "has-ansi": "^0.1.0", + "strip-ansi": "^0.3.0", + "supports-color": "^0.2.0" + } + }, + "dateformat": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.12.tgz", + "integrity": "sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk=", + "dev": true, + "requires": { + "get-stdin": "^4.0.1", + "meow": "^3.3.0" + } + }, + "gulp-util": { + "version": "2.2.20", + "resolved": "https://registry.npmjs.org/gulp-util/-/gulp-util-2.2.20.tgz", + "integrity": "sha1-1xRuVyiRC9jwR6awseVJvCLb1kw=", + "dev": true, + "requires": { + "chalk": "^0.5.0", + "dateformat": "^1.0.7-1.2.3", + "lodash._reinterpolate": "^2.4.1", + "lodash.template": "^2.4.1", + "minimist": "^0.2.0", + "multipipe": "^0.1.0", + "through2": "^0.5.0", + "vinyl": "^0.2.1" + } + }, + "has-ansi": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz", + "integrity": "sha1-hPJlqujA5qiKEtcCKJS3VoiUxi4=", + "dev": true, + "requires": { + "ansi-regex": "^0.2.0" + } + }, + "lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true + }, + "lodash._reinterpolate": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-2.4.1.tgz", + "integrity": "sha1-TxInqlqHEfxjL1sHofRgequLMiI=", + "dev": true + }, + "lodash.escape": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-2.4.1.tgz", + "integrity": "sha1-LOEsXghNsKV92l5dHu659dF1o7Q=", + "dev": true, + "requires": { + "lodash._escapehtmlchar": "~2.4.1", + "lodash._reunescapedhtml": "~2.4.1", + "lodash.keys": "~2.4.1" + } + }, + "lodash.keys": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-2.4.1.tgz", + "integrity": "sha1-SN6kbfj/djKxDXBrissmWR4rNyc=", + "dev": true, + "requires": { + "lodash._isnative": "~2.4.1", + "lodash._shimkeys": "~2.4.1", + "lodash.isobject": "~2.4.1" + } + }, + "lodash.template": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-2.4.1.tgz", + "integrity": "sha1-nmEQB+32KRKal0qzxIuBez4c8g0=", + "dev": true, + "requires": { + "lodash._escapestringchar": "~2.4.1", + "lodash._reinterpolate": "~2.4.1", + "lodash.defaults": "~2.4.1", + "lodash.escape": "~2.4.1", + "lodash.keys": "~2.4.1", + "lodash.templatesettings": "~2.4.1", + "lodash.values": "~2.4.1" + } + }, + "lodash.templatesettings": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-2.4.1.tgz", + "integrity": "sha1-6nbHXRHrhtTb6JqDiTu4YZKaxpk=", + "dev": true, + "requires": { + "lodash._reinterpolate": "~2.4.1", + "lodash.escape": "~2.4.1" + } + }, + "minimist": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.2.0.tgz", + "integrity": "sha1-Tf/lJdriuGTGbC4jxicdev3s784=", + "dev": true + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "strip-ansi": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz", + "integrity": "sha1-JfSOoiynkYfzF0pNuHWTR7sSYiA=", + "dev": true, + "requires": { + "ansi-regex": "^0.2.1" + } + }, + "supports-color": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz", + "integrity": "sha1-2S3iaU6z9nMjlz1649i1W0wiGQo=", + "dev": true + }, + "through2": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.5.1.tgz", + "integrity": "sha1-390BLrnHAOIyP9M084rGIqs3Lac=", + "dev": true, + "requires": { + "readable-stream": "~1.0.17", + "xtend": "~3.0.0" + } + }, + "vinyl": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.2.3.tgz", + "integrity": "sha1-vKk4IJWC7FpJrVOKAPofEl5RMlI=", + "dev": true, + "requires": { + "clone-stats": "~0.0.1" + } + }, + "xtend": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-3.0.0.tgz", + "integrity": "sha1-XM50B7r2Qsunvs2laBEcST9ZZlo=", + "dev": true + } + } + }, + "gulp-install": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/gulp-install/-/gulp-install-0.2.0.tgz", + "integrity": "sha1-CyL69LcVBk1D9mv3aEadU88/8Bg=", + "dev": true, + "requires": { + "gulp-util": "^2.2.14", + "through2": "^0.4.1", + "which": "^1.0.5" + }, + "dependencies": { + "ansi-regex": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz", + "integrity": "sha1-DY6UaWej2BQ/k+JOKYUl/BsiNfk=", + "dev": true + }, + "ansi-styles": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz", + "integrity": "sha1-6uy/Zs1waIJ2Cy9GkVgrj1XXp94=", + "dev": true + }, + "chalk": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz", + "integrity": "sha1-Zjs6ZItotV0EaQ1JFnqoN4WPIXQ=", + "dev": true, + "requires": { + "ansi-styles": "^1.1.0", + "escape-string-regexp": "^1.0.0", + "has-ansi": "^0.1.0", + "strip-ansi": "^0.3.0", + "supports-color": "^0.2.0" + } + }, + "dateformat": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.12.tgz", + "integrity": "sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk=", + "dev": true, + "requires": { + "get-stdin": "^4.0.1", + "meow": "^3.3.0" + } + }, + "gulp-util": { + "version": "2.2.20", + "resolved": "https://registry.npmjs.org/gulp-util/-/gulp-util-2.2.20.tgz", + "integrity": "sha1-1xRuVyiRC9jwR6awseVJvCLb1kw=", + "dev": true, + "requires": { + "chalk": "^0.5.0", + "dateformat": "^1.0.7-1.2.3", + "lodash._reinterpolate": "^2.4.1", + "lodash.template": "^2.4.1", + "minimist": "^0.2.0", + "multipipe": "^0.1.0", + "through2": "^0.5.0", + "vinyl": "^0.2.1" + }, + "dependencies": { + "through2": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.5.1.tgz", + "integrity": "sha1-390BLrnHAOIyP9M084rGIqs3Lac=", + "dev": true, + "requires": { + "readable-stream": "~1.0.17", + "xtend": "~3.0.0" + } + } + } + }, + "has-ansi": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz", + "integrity": "sha1-hPJlqujA5qiKEtcCKJS3VoiUxi4=", + "dev": true, + "requires": { + "ansi-regex": "^0.2.0" + } + }, + "lodash._reinterpolate": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-2.4.1.tgz", + "integrity": "sha1-TxInqlqHEfxjL1sHofRgequLMiI=", + "dev": true + }, + "lodash.escape": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-2.4.1.tgz", + "integrity": "sha1-LOEsXghNsKV92l5dHu659dF1o7Q=", + "dev": true, + "requires": { + "lodash._escapehtmlchar": "~2.4.1", + "lodash._reunescapedhtml": "~2.4.1", + "lodash.keys": "~2.4.1" + } + }, + "lodash.keys": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-2.4.1.tgz", + "integrity": "sha1-SN6kbfj/djKxDXBrissmWR4rNyc=", + "dev": true, + "requires": { + "lodash._isnative": "~2.4.1", + "lodash._shimkeys": "~2.4.1", + "lodash.isobject": "~2.4.1" + } + }, + "lodash.template": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-2.4.1.tgz", + "integrity": "sha1-nmEQB+32KRKal0qzxIuBez4c8g0=", + "dev": true, + "requires": { + "lodash._escapestringchar": "~2.4.1", + "lodash._reinterpolate": "~2.4.1", + "lodash.defaults": "~2.4.1", + "lodash.escape": "~2.4.1", + "lodash.keys": "~2.4.1", + "lodash.templatesettings": "~2.4.1", + "lodash.values": "~2.4.1" + } + }, + "lodash.templatesettings": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-2.4.1.tgz", + "integrity": "sha1-6nbHXRHrhtTb6JqDiTu4YZKaxpk=", + "dev": true, + "requires": { + "lodash._reinterpolate": "~2.4.1", + "lodash.escape": "~2.4.1" + } + }, + "minimist": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.2.0.tgz", + "integrity": "sha1-Tf/lJdriuGTGbC4jxicdev3s784=", + "dev": true + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "strip-ansi": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz", + "integrity": "sha1-JfSOoiynkYfzF0pNuHWTR7sSYiA=", + "dev": true, + "requires": { + "ansi-regex": "^0.2.1" + } + }, + "supports-color": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz", + "integrity": "sha1-2S3iaU6z9nMjlz1649i1W0wiGQo=", + "dev": true + }, + "through2": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.4.2.tgz", + "integrity": "sha1-2/WGYDEVHsg1K7bE22SiKSqEC5s=", + "dev": true, + "requires": { + "readable-stream": "~1.0.17", + "xtend": "~2.1.1" + }, + "dependencies": { + "xtend": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", + "integrity": "sha1-bv7MKk2tjmlixJAbM3znuoe10os=", + "dev": true, + "requires": { + "object-keys": "~0.4.0" + } + } + } + }, + "vinyl": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.2.3.tgz", + "integrity": "sha1-vKk4IJWC7FpJrVOKAPofEl5RMlI=", + "dev": true, + "requires": { + "clone-stats": "~0.0.1" + } + }, + "xtend": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-3.0.0.tgz", + "integrity": "sha1-XM50B7r2Qsunvs2laBEcST9ZZlo=", + "dev": true + } + } + }, + "gulp-rename": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/gulp-rename/-/gulp-rename-1.4.0.tgz", + "integrity": "sha512-swzbIGb/arEoFK89tPY58vg3Ok1bw+d35PfUNwWqdo7KM4jkmuGA78JiDNqR+JeZFaeeHnRg9N7aihX3YPmsyg==", + "dev": true + }, + "gulp-sketch": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/gulp-sketch/-/gulp-sketch-0.0.7.tgz", + "integrity": "sha1-dyY3TPqayYOddtbo++77Tsn34Fw=", + "dev": true, + "requires": { + "clean-sketch": "^1.0.1", + "gulp-util": "^3.0.1", + "recursive-readdir": "^1.2.0", + "rimraf": "^2.2.8", + "temporary": "0.0.8", + "through2": "^0.6.3" + }, + "dependencies": { + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "through2": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", + "dev": true, + "requires": { + "readable-stream": ">=1.0.33-1 <1.1.0-0", + "xtend": ">=4.0.0 <4.1.0-0" + } + } + } + }, + "gulp-sourcemaps": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/gulp-sourcemaps/-/gulp-sourcemaps-1.6.0.tgz", + "integrity": "sha1-uG/zSdgBzrVuHZ59x7vLS33uYAw=", + "dev": true, + "requires": { + "convert-source-map": "^1.1.1", + "graceful-fs": "^4.1.2", + "strip-bom": "^2.0.0", + "through2": "^2.0.0", + "vinyl": "^1.0.0" + }, + "dependencies": { + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "^0.2.0" + } + } + } + }, + "gulp-svg2ttf": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gulp-svg2ttf/-/gulp-svg2ttf-1.0.0.tgz", + "integrity": "sha1-2UmQy/tG3Vak2sLpFNQHsB8nPvY=", + "dev": true, + "requires": { + "bufferstreams": "0.0.2", + "gulp-util": "~3.0.1", + "readable-stream": "^1.0.33", + "svg2ttf": "~1.2.0" + }, + "dependencies": { + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + } + } + }, + "gulp-svgicons2svgfont": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gulp-svgicons2svgfont/-/gulp-svgicons2svgfont-1.0.0.tgz", + "integrity": "sha1-YQzZiTJrwylrBpD9AvbJJWqGPgQ=", + "dev": true, + "requires": { + "gulp-util": "~3.0.1", + "readable-stream": "^1.0.33", + "svgicons2svgfont": "1.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + } + } + }, + "gulp-ttf2eot": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gulp-ttf2eot/-/gulp-ttf2eot-1.0.0.tgz", + "integrity": "sha1-VhZ8hqy+FcgpcDVBvZoovssIx4c=", + "dev": true, + "requires": { + "bufferstreams": "0.0.2", + "gulp-util": "~3.0.1", + "readable-stream": "^1.0.33", + "ttf2eot": "^2.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + } + } + }, + "gulp-ttf2woff": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gulp-ttf2woff/-/gulp-ttf2woff-1.0.0.tgz", + "integrity": "sha1-1yBa3T3ZG7BWBstHqDrdz7D+duo=", + "dev": true, + "requires": { + "bufferstreams": "0.0.2", + "gulp-util": "~3.0.1", + "readable-stream": "^1.0.33", + "ttf2woff": "^2.0.1" + }, + "dependencies": { + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + } + } + }, + "gulp-util": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/gulp-util/-/gulp-util-3.0.8.tgz", + "integrity": "sha1-AFTh50RQLifATBh8PsxQXdVLu08=", + "dev": true, + "requires": { + "array-differ": "^1.0.0", + "array-uniq": "^1.0.2", + "beeper": "^1.0.0", + "chalk": "^1.0.0", + "dateformat": "^2.0.0", + "fancy-log": "^1.1.0", + "gulplog": "^1.0.0", + "has-gulplog": "^0.1.0", + "lodash._reescape": "^3.0.0", + "lodash._reevaluate": "^3.0.0", + "lodash._reinterpolate": "^3.0.0", + "lodash.template": "^3.0.0", + "minimist": "^1.1.0", + "multipipe": "^0.1.2", + "object-assign": "^3.0.0", + "replace-ext": "0.0.1", + "through2": "^2.0.0", + "vinyl": "^0.5.0" + }, + "dependencies": { + "dateformat": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-2.2.0.tgz", + "integrity": "sha1-QGXiATz5+5Ft39gu+1Bq1MZ2kGI=", + "dev": true + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "object-assign": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", + "integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=", + "dev": true + }, + "vinyl": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.5.3.tgz", + "integrity": "sha1-sEVbOPxeDPMNQyUTLkYZcMIJHN4=", + "dev": true, + "requires": { + "clone": "^1.0.0", + "clone-stats": "^0.0.1", + "replace-ext": "0.0.1" + } + } + } + }, + "gulplog": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-1.0.0.tgz", + "integrity": "sha1-4oxNRdBey77YGDY86PnFkmIp/+U=", + "dev": true, + "requires": { + "glogg": "^1.0.0" + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "dev": true + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "dev": true, + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + }, + "dependencies": { + "ajv": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", + "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + } + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-color": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/has-color/-/has-color-0.1.7.tgz", + "integrity": "sha1-ZxRKUmDDT8PMpnfQQdr1L+e3iy8=", + "dev": true + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" + }, + "has-gulplog": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/has-gulplog/-/has-gulplog-0.1.0.tgz", + "integrity": "sha1-ZBTIKRNpfaUVkDl9r7EvIpZ4Ec4=", + "dev": true, + "requires": { + "sparkles": "^1.0.0" + } + }, + "has-require": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-require/-/has-require-1.1.0.tgz", + "integrity": "sha1-QePou4YjRniT7bw5CaWJPeUsdvg=", + "dev": true + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hash-base": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", + "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "hasha": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-2.2.0.tgz", + "integrity": "sha1-eNfL/B5tZjA/55g3NlmEUXsvbuE=", + "dev": true, + "requires": { + "is-stream": "^1.0.1", + "pinkie-promise": "^2.0.0" + } + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "dev": true, + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "home-or-tmp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", + "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=", + "dev": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.1" + } + }, + "homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "dev": true, + "requires": { + "parse-passwd": "^1.0.0" + } + }, + "hooker": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", + "integrity": "sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk=", + "dev": true + }, + "hosted-git-info": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", + "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", + "dev": true + }, + "html-minifier": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-0.7.2.tgz", + "integrity": "sha1-K3lZsQUaSB5xzXxuWaZCcq+JXP0=", + "dev": true, + "requires": { + "change-case": "2.3.x", + "clean-css": "3.1.x", + "cli": "0.6.x", + "concat-stream": "1.4.x", + "relateurl": "0.2.x", + "uglify-js": "2.4.x" + }, + "dependencies": { + "async": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=", + "dev": true + }, + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", + "dev": true + }, + "clean-css": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-3.1.9.tgz", + "integrity": "sha1-29BaFIvklDuzfOBnnmdsvJ9YAmY=", + "dev": true, + "requires": { + "commander": "2.6.x", + "source-map": ">=0.1.43 <0.2" + } + }, + "commander": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.6.0.tgz", + "integrity": "sha1-nfflL7Kgyw+4kFjugMMQQiXzfh0=", + "dev": true + }, + "concat-stream": { + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.4.11.tgz", + "integrity": "sha512-X3JMh8+4je3U1cQpG87+f9lXHDrqcb2MVLg9L7o8b1UZ0DzhRrUpdn65ttzu10PpJPPI3MQNkis+oha6TSA9Mw==", + "dev": true, + "requires": { + "inherits": "~2.0.1", + "readable-stream": "~1.1.9", + "typedarray": "~0.0.5" + } + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "source-map": { + "version": "0.1.43", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", + "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", + "dev": true, + "requires": { + "amdefine": ">=0.0.4" + } + }, + "uglify-js": { + "version": "2.4.24", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.4.24.tgz", + "integrity": "sha1-+tV1XB4Vd2WLsG/5q25UjJW+vW4=", + "dev": true, + "requires": { + "async": "~0.2.6", + "source-map": "0.1.34", + "uglify-to-browserify": "~1.0.0", + "yargs": "~3.5.4" + }, + "dependencies": { + "source-map": { + "version": "0.1.34", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.34.tgz", + "integrity": "sha1-p8/omux7FoLDsZjQrPtH19CQVms=", + "dev": true, + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", + "dev": true + }, + "yargs": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.5.4.tgz", + "integrity": "sha1-2K/49mXpTDS9JZvevRv68N3TU2E=", + "dev": true, + "requires": { + "camelcase": "^1.0.2", + "decamelize": "^1.0.0", + "window-size": "0.1.0", + "wordwrap": "0.0.2" + } + } + } + }, + "htmlescape": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/htmlescape/-/htmlescape-1.1.1.tgz", + "integrity": "sha1-OgPtwiFLyjtmQko+eVk0lQnLA1E=", + "dev": true + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + } + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "https-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-0.0.1.tgz", + "integrity": "sha1-P5E2XKvmC3ftDruiS0VOPgnZWoI=", + "dev": true + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", + "dev": true + }, + "iferr": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", + "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", + "dev": true + }, + "ignore": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", + "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", + "dev": true + }, + "ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=", + "dev": true + }, + "imagemin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/imagemin/-/imagemin-4.0.0.tgz", + "integrity": "sha1-6Q5/CTaDZZXxj6Ff6Qb0+iWeqEc=", + "dev": true, + "requires": { + "buffer-to-vinyl": "^1.0.0", + "concat-stream": "^1.4.6", + "imagemin-gifsicle": "^4.0.0", + "imagemin-jpegtran": "^4.0.0", + "imagemin-optipng": "^4.0.0", + "imagemin-svgo": "^4.0.0", + "optional": "^0.1.0", + "readable-stream": "^2.0.0", + "stream-combiner2": "^1.1.1", + "vinyl-fs": "^2.1.1" + } + }, + "imagemin-gifsicle": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/imagemin-gifsicle/-/imagemin-gifsicle-4.2.0.tgz", + "integrity": "sha1-D++butNHbmt2iFc2zFsLh6CHV8o=", + "dev": true, + "optional": true, + "requires": { + "gifsicle": "^3.0.0", + "is-gif": "^1.0.0", + "through2": "^0.6.1" + }, + "dependencies": { + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "through2": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", + "dev": true, + "optional": true, + "requires": { + "readable-stream": ">=1.0.33-1 <1.1.0-0", + "xtend": ">=4.0.0 <4.1.0-0" + } + } + } + }, + "imagemin-jpegtran": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/imagemin-jpegtran/-/imagemin-jpegtran-4.3.2.tgz", + "integrity": "sha1-G8bR4r0T/bZNJFUm1jWn5d/rEvw=", + "dev": true, + "optional": true, + "requires": { + "is-jpg": "^1.0.0", + "jpegtran-bin": "^3.0.0", + "through2": "^2.0.0" + } + }, + "imagemin-optipng": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/imagemin-optipng/-/imagemin-optipng-4.3.0.tgz", + "integrity": "sha1-dgRmOrLuMVczJ0cm/Rw3TStErbY=", + "dev": true, + "optional": true, + "requires": { + "exec-buffer": "^2.0.0", + "is-png": "^1.0.0", + "optipng-bin": "^3.0.0", + "through2": "^0.6.1" + }, + "dependencies": { + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "through2": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", + "dev": true, + "optional": true, + "requires": { + "readable-stream": ">=1.0.33-1 <1.1.0-0", + "xtend": ">=4.0.0 <4.1.0-0" + } + } + } + }, + "imagemin-svgo": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/imagemin-svgo/-/imagemin-svgo-4.2.1.tgz", + "integrity": "sha1-VPB9xW9HJgRi32phxUvvtEtXvlU=", + "dev": true, + "optional": true, + "requires": { + "is-svg": "^1.0.0", + "svgo": "^0.6.0", + "through2": "^2.0.0" + } + }, + "import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", + "dev": true + }, + "import-local": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", + "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", + "dev": true, + "requires": { + "pkg-dir": "^3.0.0", + "resolve-cwd": "^2.0.0" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", + "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + } + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "in-publish": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz", + "integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E=", + "dev": true + }, + "indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "dev": true, + "requires": { + "repeating": "^2.0.0" + } + }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "dev": true + }, + "inline-source-map": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/inline-source-map/-/inline-source-map-0.6.2.tgz", + "integrity": "sha1-+Tk0ccGKedFyT4Y/o4tYY3Ct4qU=", + "dev": true, + "requires": { + "source-map": "~0.5.3" + } + }, + "inquirer": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz", + "integrity": "sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==", + "dev": true, + "requires": { + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.0", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^2.0.4", + "figures": "^2.0.0", + "lodash": "^4.3.0", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rx-lite": "^4.0.8", + "rx-lite-aggregates": "^4.0.8", + "string-width": "^2.1.0", + "strip-ansi": "^4.0.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "insert-module-globals": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/insert-module-globals/-/insert-module-globals-7.2.0.tgz", + "integrity": "sha512-VE6NlW+WGn2/AeOMd496AHFYmE7eLKkUY6Ty31k4og5vmA3Fjuwe9v6ifH6Xx/Hz27QvdoMoviw1/pqWRB09Sw==", + "dev": true, + "requires": { + "JSONStream": "^1.0.3", + "acorn-node": "^1.5.2", + "combine-source-map": "^0.8.0", + "concat-stream": "^1.6.1", + "is-buffer": "^1.1.0", + "path-is-absolute": "^1.0.1", + "process": "~0.11.0", + "through2": "^2.0.0", + "undeclared-identifiers": "^1.1.2", + "xtend": "^4.0.0" + }, + "dependencies": { + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + } + } + }, + "interpret": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-0.3.10.tgz", + "integrity": "sha1-CIwl3nMcbFsRKpDwBxz69Fnlp7s=", + "dev": true + }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "requires": { + "loose-envify": "^1.0.0" + } + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", + "dev": true + }, + "ip-regex": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-1.0.3.tgz", + "integrity": "sha1-3FiQdvZZ9BnCIgOaMzFvHHOH7/0=", + "dev": true, + "optional": true + }, + "is-absolute": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-0.1.7.tgz", + "integrity": "sha1-hHSREZ/MtftDYhfMc39/qtUPYD8=", + "dev": true, + "optional": true, + "requires": { + "is-relative": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-bzip2": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-bzip2/-/is-bzip2-1.0.0.tgz", + "integrity": "sha1-XuWOqlounIDiFAe+3yOuWsCRs/w=", + "dev": true, + "optional": true + }, + "is-ci": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", + "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", + "dev": true, + "requires": { + "ci-info": "^1.5.0" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-defined": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-defined/-/is-defined-1.0.0.tgz", + "integrity": "sha1-HwfKZ9Vx9ZTEsUQVpF9774j5K/U=", + "dev": true + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", + "dev": true + }, + "is-equal-shallow": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", + "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "dev": true, + "requires": { + "is-primitive": "^2.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "is-gif": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-gif/-/is-gif-1.0.0.tgz", + "integrity": "sha1-ptKumIkwB7/6l6HYwB1jIFgyCX4=", + "dev": true, + "optional": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-gzip": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-gzip/-/is-gzip-1.0.0.tgz", + "integrity": "sha1-bKiwe5nHeZgCWQDlVc7Y7YCHmoM=", + "dev": true, + "optional": true + }, + "is-installed-globally": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz", + "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=", + "dev": true, + "requires": { + "global-dirs": "^0.1.0", + "is-path-inside": "^1.0.0" + } + }, + "is-jpg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-jpg/-/is-jpg-1.0.1.tgz", + "integrity": "sha1-KW1X/dmc4BBDSnKD40armhA16XU=", + "dev": true, + "optional": true + }, + "is-lower-case": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-lower-case/-/is-lower-case-1.1.3.tgz", + "integrity": "sha1-fhR75HaNxGbbO/shzGCzHmrWk5M=", + "dev": true, + "requires": { + "lower-case": "^1.1.0" + } + }, + "is-natural-number": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-2.1.1.tgz", + "integrity": "sha1-fUxXKDd+84bD4ZSpkRv1fG3DNec=", + "dev": true, + "optional": true + }, + "is-npm": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", + "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=", + "dev": true + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", + "dev": true + }, + "is-path-inside": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", + "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", + "dev": true, + "requires": { + "path-is-inside": "^1.0.1" + } + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "is-png": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-png/-/is-png-1.1.0.tgz", + "integrity": "sha1-1XSxK/J1wDUEVVcLDltXqwYgd84=", + "dev": true, + "optional": true + }, + "is-posix-bracket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", + "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", + "dev": true + }, + "is-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", + "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", + "dev": true + }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", + "dev": true + }, + "is-redirect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", + "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=", + "dev": true + }, + "is-relative": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-0.1.3.tgz", + "integrity": "sha1-kF/uiuhvRbPsYUvDwVyGnfCHboI=", + "dev": true, + "optional": true + }, + "is-resolvable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", + "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", + "dev": true + }, + "is-retry-allowed": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz", + "integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-svg": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-1.1.1.tgz", + "integrity": "sha1-rA76r7ZTrFhHNwix+HNjbKEQ4xs=", + "dev": true, + "optional": true + }, + "is-tar": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-tar/-/is-tar-1.0.0.tgz", + "integrity": "sha1-L2suF5LB9bs2UZrKqdZcDSb+hT0=", + "dev": true, + "optional": true + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "is-upper-case": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-upper-case/-/is-upper-case-1.1.2.tgz", + "integrity": "sha1-jQsfp+eTOh5YSDYA7H2WYcuvdW8=", + "dev": true, + "requires": { + "upper-case": "^1.1.0" + } + }, + "is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", + "dev": true, + "optional": true + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true + }, + "is-valid-glob": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-0.3.0.tgz", + "integrity": "sha1-1LVcafUYhvm2XHDWwmItN+KfSP4=", + "dev": true + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "is-zip": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-zip/-/is-zip-1.0.0.tgz", + "integrity": "sha1-R7Co/004p2QxzP2ZqOFaTIa6IyU=", + "dev": true, + "optional": true + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "jasmine-ajax": { + "version": "git://github.com/nobuti/jasmine-ajax.git#7b1047a4b6c8e8c0e7dc5419059ed6f2c3fc00f4", + "from": "git://github.com/nobuti/jasmine-ajax.git#master", + "dev": true + }, + "jasmine-core": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.4.1.tgz", + "integrity": "sha1-b4OrOg8WlRcizgfSBsdz1XzIOL4=", + "dev": true + }, + "jpegtran-bin": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jpegtran-bin/-/jpegtran-bin-3.2.0.tgz", + "integrity": "sha1-9g7PSumZwL2tLp+83ytvCYHnops=", + "dev": true, + "optional": true, + "requires": { + "bin-build": "^2.0.0", + "bin-wrapper": "^3.0.0", + "logalot": "^2.0.0" + } + }, + "jquery": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-2.1.4.tgz", + "integrity": "sha1-IoveaYoMYUMdwmMKahVPFYkNIxc=" + }, + "js-base64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.1.tgz", + "integrity": "sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw==" + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "dependencies": { + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + } + } + }, + "js2xmlparser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-3.0.0.tgz", + "integrity": "sha1-P7YOqgicVED5MZ9RdgzNB+JJlzM=", + "dev": true, + "requires": { + "xmlcreate": "^1.0.1" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true + }, + "jsdoc": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-3.5.5.tgz", + "integrity": "sha512-6PxB65TAU4WO0Wzyr/4/YhlGovXl0EVYfpKbpSroSj0qBxT4/xod/l40Opkm38dRHRdQgdeY836M0uVnJQG7kg==", + "dev": true, + "requires": { + "babylon": "7.0.0-beta.19", + "bluebird": "~3.5.0", + "catharsis": "~0.8.9", + "escape-string-regexp": "~1.0.5", + "js2xmlparser": "~3.0.0", + "klaw": "~2.0.0", + "marked": "~0.3.6", + "mkdirp": "~0.5.1", + "requizzle": "~0.2.1", + "strip-json-comments": "~2.0.1", + "taffydb": "2.6.2", + "underscore": "~1.8.3" + }, + "dependencies": { + "babylon": { + "version": "7.0.0-beta.19", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.19.tgz", + "integrity": "sha512-Vg0C9s/REX6/WIXN37UKpv5ZhRi6A4pjHlpkE34+8/a6c2W1Q692n3hmc+SZG5lKRnaExLUbxtJ1SVT+KaCQ/A==", + "dev": true + }, + "klaw": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-2.0.0.tgz", + "integrity": "sha1-WcEo4Nxc5BAgEVEZTuucv4WGUPY=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.9" + } + } + } + }, + "jsesc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", + "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", + "dev": true + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", + "dev": true + }, + "json-stable-stringify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-0.0.1.tgz", + "integrity": "sha1-YRwj6BTbN1Un34URk9tZ3Sryf0U=", + "dev": true, + "requires": { + "jsonify": "~0.0.0" + } + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "dev": true + }, + "jsonfile": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", + "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", + "dev": true + }, + "jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", + "dev": true + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "jstify": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/jstify/-/jstify-0.12.0.tgz", + "integrity": "sha1-BZ1/kKyUPEh+4hYhHU2+F3vvbo8=", + "dev": true, + "requires": { + "html-minifier": "^0.7.2", + "lodash.escape": "~3.0.0", + "through2": "^0.6.3", + "underscore": "^1.7.0" + }, + "dependencies": { + "lodash.escape": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-3.0.0.tgz", + "integrity": "sha1-+ylMmae/tYYDn2bWucJ+2HTLe1E=", + "dev": true, + "requires": { + "lodash._basetostring": "^3.0.0" + } + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "through2": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", + "dev": true, + "requires": { + "readable-stream": ">=1.0.33-1 <1.1.0-0", + "xtend": ">=4.0.0 <4.1.0-0" + } + } + } + }, + "kew": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/kew/-/kew-0.7.0.tgz", + "integrity": "sha1-edk9LTM2PW/dKXCzNdkUGtWR15s=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "klaw": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz", + "integrity": "sha1-QIhDO0azsbolnXh4XY6W9zugJDk=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.9" + } + }, + "labeled-stream-splicer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/labeled-stream-splicer/-/labeled-stream-splicer-2.0.2.tgz", + "integrity": "sha512-Ca4LSXFFZUjPScRaqOcFxneA0VpKZr4MMYCljyQr4LIewTLb3Y0IUTIsnBBsVubIeEfxeSZpSjSsRM8APEQaAw==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "stream-splicer": "^2.0.0" + } + }, + "latest-version": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", + "integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=", + "dev": true, + "requires": { + "package-json": "^4.0.0" + } + }, + "lazy-req": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/lazy-req/-/lazy-req-1.1.0.tgz", + "integrity": "sha1-va6+rTD42CQDnODOFJ1Nqge6H6w=", + "dev": true, + "optional": true + }, + "lazystream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", + "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", + "dev": true, + "requires": { + "readable-stream": "^2.0.5" + } + }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "dev": true, + "requires": { + "invert-kv": "^1.0.0" + } + }, + "leaflet": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.3.1.tgz", + "integrity": "sha512-adQOIzh+bfdridLM1xIgJ9VnJbAUY3wqs/ueF+ITla+PLQ1z47USdBKUf+iD9FuUA8RtlT6j6hZBfZoA6mW+XQ==", + "dev": true + }, + "leb": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/leb/-/leb-0.3.0.tgz", + "integrity": "sha1-Mr7p+tFoMo1q6oUi2DP0GA7tHaM=", + "dev": true + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "lexical-scope": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/lexical-scope/-/lexical-scope-1.2.0.tgz", + "integrity": "sha1-/Ope3HBKSzqHls3KQZw6CvryLfQ=", + "dev": true, + "requires": { + "astw": "^2.0.0" + } + }, + "liftoff": { + "version": "0.13.6", + "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-0.13.6.tgz", + "integrity": "sha1-YA6JZrktHgFQ6rW1d2UlafTH0dg=", + "dev": true, + "requires": { + "extend": "~1.3.0", + "findup-sync": "~0.1.2", + "flagged-respawn": "~0.3.0", + "minimist": "~1.1.0", + "resolve": "~1.0.0" + }, + "dependencies": { + "extend": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extend/-/extend-1.3.0.tgz", + "integrity": "sha1-0VFvsP9WJNLr+RI+odrFoZlABPg=", + "dev": true + }, + "minimist": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.1.3.tgz", + "integrity": "sha1-O+39kaktOQFvz6ocaB6Pqhoe/ag=", + "dev": true + }, + "resolve": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.0.0.tgz", + "integrity": "sha1-Km47MU3NV8ZRno4igq+Gh+jeYcY=", + "dev": true + } + } + }, + "load-grunt-tasks": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/load-grunt-tasks/-/load-grunt-tasks-0.6.0.tgz", + "integrity": "sha1-BDwErWnsyF4CqCJY/fJbenng22w=", + "dev": true, + "requires": { + "findup-sync": "^0.1.2", + "multimatch": "^0.3.0" + } + }, + "load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "loader-runner": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", + "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==", + "dev": true + }, + "loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + }, + "dependencies": { + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, + "lodash._basecopy": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", + "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", + "dev": true + }, + "lodash._basetostring": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._basetostring/-/lodash._basetostring-3.0.1.tgz", + "integrity": "sha1-0YYdh3+CSlL2aYMtyvPuFVZqB9U=", + "dev": true + }, + "lodash._basevalues": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._basevalues/-/lodash._basevalues-3.0.0.tgz", + "integrity": "sha1-W3dXYoAr3j0yl1A+JjAIIP32Ybc=", + "dev": true + }, + "lodash._escapehtmlchar": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash._escapehtmlchar/-/lodash._escapehtmlchar-2.4.1.tgz", + "integrity": "sha1-32fDu2t+jh6DGrSL+geVuSr+iZ0=", + "dev": true, + "requires": { + "lodash._htmlescapes": "~2.4.1" + } + }, + "lodash._escapestringchar": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash._escapestringchar/-/lodash._escapestringchar-2.4.1.tgz", + "integrity": "sha1-7P4iYYoq3lC/7qQ5N+Ud9m8O23I=", + "dev": true + }, + "lodash._getnative": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", + "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", + "dev": true + }, + "lodash._htmlescapes": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash._htmlescapes/-/lodash._htmlescapes-2.4.1.tgz", + "integrity": "sha1-MtFL8IRLbeb4tioFG09nwii2JMs=", + "dev": true + }, + "lodash._isiterateecall": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", + "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", + "dev": true + }, + "lodash._isnative": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash._isnative/-/lodash._isnative-2.4.1.tgz", + "integrity": "sha1-PqZAS3hKe+g2x7V1gOHN95sUgyw=", + "dev": true + }, + "lodash._objecttypes": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash._objecttypes/-/lodash._objecttypes-2.4.1.tgz", + "integrity": "sha1-fAt/admKH3ZSn4kLDNsbTf7BHBE=", + "dev": true + }, + "lodash._reescape": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reescape/-/lodash._reescape-3.0.0.tgz", + "integrity": "sha1-Kx1vXf4HyKNVdT5fJ/rH8c3hYWo=", + "dev": true + }, + "lodash._reevaluate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reevaluate/-/lodash._reevaluate-3.0.0.tgz", + "integrity": "sha1-WLx0xAZklTrgsSTYBpltrKQx4u0=", + "dev": true + }, + "lodash._reinterpolate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", + "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=", + "dev": true + }, + "lodash._reunescapedhtml": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash._reunescapedhtml/-/lodash._reunescapedhtml-2.4.1.tgz", + "integrity": "sha1-dHxPxAED6zu4oJduVx96JlnpO6c=", + "dev": true, + "requires": { + "lodash._htmlescapes": "~2.4.1", + "lodash.keys": "~2.4.1" + }, + "dependencies": { + "lodash.keys": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-2.4.1.tgz", + "integrity": "sha1-SN6kbfj/djKxDXBrissmWR4rNyc=", + "dev": true, + "requires": { + "lodash._isnative": "~2.4.1", + "lodash._shimkeys": "~2.4.1", + "lodash.isobject": "~2.4.1" + } + } + } + }, + "lodash._root": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._root/-/lodash._root-3.0.1.tgz", + "integrity": "sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI=", + "dev": true + }, + "lodash._shimkeys": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash._shimkeys/-/lodash._shimkeys-2.4.1.tgz", + "integrity": "sha1-bpzJZm/wgfC1psl4uD4kLmlJ0gM=", + "dev": true, + "requires": { + "lodash._objecttypes": "~2.4.1" + } + }, + "lodash.cond": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/lodash.cond/-/lodash.cond-4.5.2.tgz", + "integrity": "sha1-9HGh2khr5g9quVXRcRVSPdHSVdU=", + "dev": true + }, + "lodash.defaults": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-2.4.1.tgz", + "integrity": "sha1-p+iIXwXmiFEUS24SqPNngCa8TFQ=", + "dev": true, + "requires": { + "lodash._objecttypes": "~2.4.1", + "lodash.keys": "~2.4.1" + }, + "dependencies": { + "lodash.keys": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-2.4.1.tgz", + "integrity": "sha1-SN6kbfj/djKxDXBrissmWR4rNyc=", + "dev": true, + "requires": { + "lodash._isnative": "~2.4.1", + "lodash._shimkeys": "~2.4.1", + "lodash.isobject": "~2.4.1" + } + } + } + }, + "lodash.escape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-3.2.0.tgz", + "integrity": "sha1-mV7g3BjBtIzJLv+ucaEKq1tIdpg=", + "dev": true, + "requires": { + "lodash._root": "^3.0.0" + } + }, + "lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", + "dev": true + }, + "lodash.isarray": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", + "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", + "dev": true + }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=", + "dev": true + }, + "lodash.isobject": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-2.4.1.tgz", + "integrity": "sha1-Wi5H/mmVPx7mMafrof5k0tBlWPU=", + "dev": true, + "requires": { + "lodash._objecttypes": "~2.4.1" + } + }, + "lodash.keys": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", + "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", + "dev": true, + "requires": { + "lodash._getnative": "^3.0.0", + "lodash.isarguments": "^3.0.0", + "lodash.isarray": "^3.0.0" + } + }, + "lodash.memoize": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz", + "integrity": "sha1-LcvSwofLwKVcxCMovQxzYVDVPj8=", + "dev": true + }, + "lodash.restparam": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz", + "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=", + "dev": true + }, + "lodash.template": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-3.6.2.tgz", + "integrity": "sha1-+M3sxhaaJVvpCYrosMU9N4kx0U8=", + "dev": true, + "requires": { + "lodash._basecopy": "^3.0.0", + "lodash._basetostring": "^3.0.0", + "lodash._basevalues": "^3.0.0", + "lodash._isiterateecall": "^3.0.0", + "lodash._reinterpolate": "^3.0.0", + "lodash.escape": "^3.0.0", + "lodash.keys": "^3.0.0", + "lodash.restparam": "^3.0.0", + "lodash.templatesettings": "^3.0.0" + } + }, + "lodash.templatesettings": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz", + "integrity": "sha1-+zB4RHU7Zrnxr6VOJix0UwfbqOU=", + "dev": true, + "requires": { + "lodash._reinterpolate": "^3.0.0", + "lodash.escape": "^3.0.0" + } + }, + "lodash.values": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.values/-/lodash.values-2.4.1.tgz", + "integrity": "sha1-q/UUQ2s8twUAFieXjLzzCxKA7qQ=", + "dev": true, + "requires": { + "lodash.keys": "~2.4.1" + }, + "dependencies": { + "lodash.keys": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-2.4.1.tgz", + "integrity": "sha1-SN6kbfj/djKxDXBrissmWR4rNyc=", + "dev": true, + "requires": { + "lodash._isnative": "~2.4.1", + "lodash._shimkeys": "~2.4.1", + "lodash.isobject": "~2.4.1" + } + } + } + }, + "logalot": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/logalot/-/logalot-2.1.0.tgz", + "integrity": "sha1-X46MkNME7fElMJUaVVSruMXj9VI=", + "dev": true, + "optional": true, + "requires": { + "figures": "^1.3.5", + "squeak": "^1.0.0" + }, + "dependencies": { + "figures": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "dev": true, + "optional": true, + "requires": { + "escape-string-regexp": "^1.0.5", + "object-assign": "^4.1.0" + } + } + } + }, + "long": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz", + "integrity": "sha1-2CG3E4yhy1gcFymQ7xTbIAtcR0s=", + "dev": true + }, + "longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", + "dev": true, + "optional": true + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "dev": true, + "requires": { + "currently-unhandled": "^0.4.1", + "signal-exit": "^3.0.0" + } + }, + "lower-case": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", + "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=", + "dev": true + }, + "lower-case-first": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/lower-case-first/-/lower-case-first-1.0.2.tgz", + "integrity": "sha1-5dp8JvKacHO+AtUrrJmA5ZIq36E=", + "dev": true, + "requires": { + "lower-case": "^1.1.2" + } + }, + "lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "dev": true + }, + "lpad-align": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/lpad-align/-/lpad-align-1.1.2.tgz", + "integrity": "sha1-IfYArBwwlcPG5JfuZyce4ISB/p4=", + "dev": true, + "optional": true, + "requires": { + "get-stdin": "^4.0.1", + "indent-string": "^2.1.0", + "longest": "^1.0.0", + "meow": "^3.3.0" + } + }, + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "mamacro": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/mamacro/-/mamacro-0.0.3.tgz", + "integrity": "sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA==", + "dev": true + }, + "map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "dev": true, + "requires": { + "p-defer": "^1.0.0" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true + }, + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "requires": { + "object-visit": "^1.0.0" + } + }, + "mapnik-reference": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/mapnik-reference/-/mapnik-reference-6.0.5.tgz", + "integrity": "sha1-1SMZ4d+I5aB6gR6E3vKhpCO28F8=" + }, + "marked": { + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.19.tgz", + "integrity": "sha512-ea2eGWOqNxPcXv8dyERdSr/6FmzvWwzjMxpfGB/sbMccXoct+xY+YukPD+QTUZwyvK7BZwcr4m21WBOW41pAkg==", + "dev": true + }, + "math-random": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.4.tgz", + "integrity": "sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==", + "dev": true + }, + "md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "dev": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "mem": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", + "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", + "dev": true, + "requires": { + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^2.0.0", + "p-is-promise": "^2.0.0" + }, + "dependencies": { + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + } + } + }, + "memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "dev": true, + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + }, + "meow": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "dev": true, + "requires": { + "camelcase-keys": "^2.0.0", + "decamelize": "^1.1.2", + "loud-rejection": "^1.0.0", + "map-obj": "^1.0.1", + "minimist": "^1.1.3", + "normalize-package-data": "^2.3.4", + "object-assign": "^4.0.1", + "read-pkg-up": "^1.0.1", + "redent": "^1.0.0", + "trim-newlines": "^1.0.0" + }, + "dependencies": { + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "^2.0.0" + } + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "requires": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "requires": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "^0.2.0" + } + } + } + }, + "merge-stream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-1.0.1.tgz", + "integrity": "sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE=", + "dev": true, + "requires": { + "readable-stream": "^2.0.1" + } + }, + "microbuffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/microbuffer/-/microbuffer-1.0.0.tgz", + "integrity": "sha1-izgy7UDIfVH0e7I0kTppinVtGdI=", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + } + }, + "mime": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz", + "integrity": "sha1-WCA+7Ybjpe8XrtK32evUfwpg3RA=", + "dev": true + }, + "mime-db": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==", + "dev": true + }, + "mime-types": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "dev": true, + "requires": { + "mime-db": "1.40.0" + } + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=" + }, + "mississippi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-2.0.0.tgz", + "integrity": "sha512-zHo8v+otD1J10j/tC+VNoGK9keCuByhKovAvdn74dmxJl9+mWHnx6EMsDN4lgRoMI/eYo2nchAxniIbUPb5onw==", + "dev": true, + "requires": { + "concat-stream": "^1.5.0", + "duplexify": "^3.4.2", + "end-of-stream": "^1.1.0", + "flush-write-stream": "^1.0.0", + "from2": "^2.1.0", + "parallel-transform": "^1.1.0", + "pump": "^2.0.1", + "pumpify": "^1.3.3", + "stream-each": "^1.1.0", + "through2": "^2.0.0" + } + }, + "mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "dev": true, + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + } + } + }, + "module-deps": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-4.1.1.tgz", + "integrity": "sha1-IyFYM/HaE/1gbMuAh7RIUty4If0=", + "dev": true, + "requires": { + "JSONStream": "^1.0.3", + "browser-resolve": "^1.7.0", + "cached-path-relative": "^1.0.0", + "concat-stream": "~1.5.0", + "defined": "^1.0.0", + "detective": "^4.0.0", + "duplexer2": "^0.1.2", + "inherits": "^2.0.1", + "parents": "^1.0.0", + "readable-stream": "^2.0.2", + "resolve": "^1.1.3", + "stream-combiner2": "^1.1.1", + "subarg": "^1.0.0", + "through2": "^2.0.0", + "xtend": "^4.0.0" + } + }, + "mold-source-map": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/mold-source-map/-/mold-source-map-0.4.0.tgz", + "integrity": "sha1-z2fgsxxHq5uttcnCVlGGISe7gxc=", + "dev": true, + "requires": { + "convert-source-map": "^1.1.0", + "through": "~2.2.7" + }, + "dependencies": { + "through": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/through/-/through-2.2.7.tgz", + "integrity": "sha1-bo4hIAGR1OtqmfbwEN9Gqhxusr0=", + "dev": true + } + } + }, + "morgan": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.1.tgz", + "integrity": "sha512-HQStPIV4y3afTiCYVxirakhlCfGkI161c76kKFca7Fk1JusM//Qeo1ej2XaMniiNeaZklMVrh3vTtIzpzwbpmA==", + "dev": true, + "requires": { + "basic-auth": "~2.0.0", + "debug": "2.6.9", + "depd": "~1.1.2", + "on-finished": "~2.3.0", + "on-headers": "~1.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "mothership": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/mothership/-/mothership-0.2.0.tgz", + "integrity": "sha1-k9SKL7w+UOKl/I7VhvW8RMZfmpk=", + "dev": true, + "requires": { + "find-parent-dir": "~0.3.0" + } + }, + "move-concurrently": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", + "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", + "dev": true, + "requires": { + "aproba": "^1.1.1", + "copy-concurrently": "^1.0.0", + "fs-write-stream-atomic": "^1.0.8", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.3" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "multimatch": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-0.3.0.tgz", + "integrity": "sha1-YD28P+MoHTOAlKHhuTqLXyvgONo=", + "dev": true, + "requires": { + "array-differ": "^0.1.0", + "array-union": "^0.1.0", + "minimatch": "^0.3.0" + }, + "dependencies": { + "array-differ": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-0.1.0.tgz", + "integrity": "sha1-EuLJtwa+1HyLSDtX5IdHP7CGHzo=", + "dev": true + }, + "lru-cache": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", + "dev": true + }, + "minimatch": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", + "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", + "dev": true, + "requires": { + "lru-cache": "2", + "sigmund": "~1.0.0" + } + } + } + }, + "multipipe": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/multipipe/-/multipipe-0.1.2.tgz", + "integrity": "sha1-Ko8t33Du1WTf8tV/HhoTfZ8FB4s=", + "dev": true, + "requires": { + "duplexer2": "0.0.2" + }, + "dependencies": { + "duplexer2": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz", + "integrity": "sha1-xhTc9n4vsUmVqRcR5aYX6KYKMds=", + "dev": true, + "requires": { + "readable-stream": "~1.1.9" + } + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + } + } + }, + "mustache": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-1.1.0.tgz", + "integrity": "sha1-DZpVS0s/niSObCdCwledYY0v8A8=" + }, + "mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", + "dev": true + }, + "nan": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", + "dev": true + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + } + }, + "natives": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/natives/-/natives-1.1.6.tgz", + "integrity": "sha512-6+TDFewD4yxY14ptjKaS63GVdtKiES1pTPyxn9Jb0rBqPMZ7VcCiooEhPNsr+mqHtMGxa/5c/HhcC4uPEUw/nA==", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "nave": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/nave/-/nave-0.5.3.tgz", + "integrity": "sha1-Ws7HI3WFblx2yDvSGmjXE+tfG6Q=", + "dev": true + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", + "dev": true + }, + "neo-async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", + "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node-gyp": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", + "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==", + "dev": true, + "requires": { + "fstream": "^1.0.0", + "glob": "^7.0.3", + "graceful-fs": "^4.1.2", + "mkdirp": "^0.5.0", + "nopt": "2 || 3", + "npmlog": "0 || 1 || 2 || 3 || 4", + "osenv": "0", + "request": "^2.87.0", + "rimraf": "2", + "semver": "~5.3.0", + "tar": "^2.0.0", + "which": "1" + }, + "dependencies": { + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "dev": true, + "requires": { + "abbrev": "1" + } + }, + "semver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", + "dev": true + } + } + }, + "node-libs-browser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", + "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", + "dev": true, + "requires": { + "assert": "^1.1.1", + "browserify-zlib": "^0.2.0", + "buffer": "^4.3.0", + "console-browserify": "^1.1.0", + "constants-browserify": "^1.0.0", + "crypto-browserify": "^3.11.0", + "domain-browser": "^1.1.1", + "events": "^3.0.0", + "https-browserify": "^1.0.0", + "os-browserify": "^0.3.0", + "path-browserify": "0.0.1", + "process": "^0.11.10", + "punycode": "^1.2.4", + "querystring-es3": "^0.2.0", + "readable-stream": "^2.3.3", + "stream-browserify": "^2.0.1", + "stream-http": "^2.7.2", + "string_decoder": "^1.0.0", + "timers-browserify": "^2.0.4", + "tty-browserify": "0.0.0", + "url": "^0.11.0", + "util": "^0.11.0", + "vm-browserify": "^1.0.1" + }, + "dependencies": { + "browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, + "requires": { + "pako": "~1.0.5" + } + }, + "events": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.0.0.tgz", + "integrity": "sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA==", + "dev": true + }, + "https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", + "dev": true + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", + "dev": true + }, + "pako": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz", + "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==", + "dev": true + }, + "string_decoder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz", + "integrity": "sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "timers-browserify": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.10.tgz", + "integrity": "sha512-YvC1SV1XdOUaL6gx5CoGroT3Gu49pK9+TZ38ErPldOWW4j49GI1HKs9DV+KGq/w6y+LZ72W1c8cKz2vzY+qpzg==", + "dev": true, + "requires": { + "setimmediate": "^1.0.4" + } + }, + "tty-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", + "dev": true + }, + "util": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", + "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", + "dev": true, + "requires": { + "inherits": "2.0.3" + } + }, + "vm-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.0.tgz", + "integrity": "sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw==", + "dev": true + } + } + }, + "node-sass": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.12.0.tgz", + "integrity": "sha512-A1Iv4oN+Iel6EPv77/HddXErL2a+gZ4uBeZUy+a8O35CFYTXhgA8MgLCWBtwpGZdCvTvQ9d+bQxX/QC36GDPpQ==", + "dev": true, + "requires": { + "async-foreach": "^0.1.3", + "chalk": "^1.1.1", + "cross-spawn": "^3.0.0", + "gaze": "^1.0.0", + "get-stdin": "^4.0.1", + "glob": "^7.0.3", + "in-publish": "^2.0.0", + "lodash": "^4.17.11", + "meow": "^3.7.0", + "mkdirp": "^0.5.1", + "nan": "^2.13.2", + "node-gyp": "^3.8.0", + "npmlog": "^4.0.0", + "request": "^2.88.0", + "sass-graph": "^2.2.4", + "stdout-stream": "^1.4.0", + "true-case-path": "^1.0.2" + }, + "dependencies": { + "cross-spawn": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", + "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "which": "^1.2.9" + } + }, + "gaze": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", + "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", + "dev": true, + "requires": { + "globule": "^1.0.0" + } + }, + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "globule": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz", + "integrity": "sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ==", + "dev": true, + "requires": { + "glob": "~7.1.1", + "lodash": "~4.17.10", + "minimatch": "~3.0.2" + } + } + } + }, + "node-status-codes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-status-codes/-/node-status-codes-1.0.0.tgz", + "integrity": "sha1-WuVUHQJGRdMqWPzdyc7s6nrjrC8=", + "dev": true, + "optional": true + }, + "nodemon": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.19.1.tgz", + "integrity": "sha512-/DXLzd/GhiaDXXbGId5BzxP1GlsqtMGM9zTmkWrgXtSqjKmGSbLicM/oAy4FR0YWm14jCHRwnR31AHS2dYFHrg==", + "dev": true, + "requires": { + "chokidar": "^2.1.5", + "debug": "^3.1.0", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.0.4", + "pstree.remy": "^1.1.6", + "semver": "^5.5.0", + "supports-color": "^5.2.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.2", + "update-notifier": "^2.5.0" + }, + "dependencies": { + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "dev": true, + "requires": { + "abbrev": "1" + } + }, + "noptify": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/noptify/-/noptify-0.0.3.tgz", + "integrity": "sha1-WPZUpz2XU98MUdlobckhBKZ/S7s=", + "dev": true, + "requires": { + "nopt": "~2.0.0" + }, + "dependencies": { + "nopt": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-2.0.0.tgz", + "integrity": "sha1-ynQW8gpeP5w7hhgPlilfo9C1Lg0=", + "dev": true, + "requires": { + "abbrev": "1" + } + } + } + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + }, + "npm-watch": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/npm-watch/-/npm-watch-0.3.0.tgz", + "integrity": "sha512-w/czxbmI2B/L7eotpo8wfE5u92X6Oguy+L3ECgfUUaERNSAtIHR0djYqzGVD7BakCH+HE3hHoDxV798rug/A6g==", + "dev": true, + "requires": { + "nodemon": "^1.12.1", + "through2": "^2.0.0" + } + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "dev": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "object-keys": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", + "integrity": "sha1-KKaq50KN0sOpLz2V8hM13SBOAzY=", + "dev": true + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "requires": { + "isobject": "^3.0.0" + } + }, + "object.omit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", + "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "dev": true, + "requires": { + "for-own": "^0.1.4", + "is-extendable": "^0.1.1" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "dev": true, + "requires": { + "mimic-fn": "^1.0.0" + } + }, + "opn": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/opn/-/opn-1.0.2.tgz", + "integrity": "sha1-uQlkM0bQChq8l3qLlvPOPFPVz18=", + "dev": true + }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "requires": { + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" + } + }, + "optional": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/optional/-/optional-0.1.4.tgz", + "integrity": "sha512-gtvrrCfkE08wKcgXaVwQVgwEQ8vel2dc5DDBn9RLQZ3YtmtkBss6A2HY6BnJH4N/4Ku97Ri/SF8sNWE2225WJw==", + "dev": true + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "optipng-bin": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/optipng-bin/-/optipng-bin-3.1.4.tgz", + "integrity": "sha1-ldNPLEiHBPb9cGBr/qDGWfHZXYQ=", + "dev": true, + "optional": true, + "requires": { + "bin-build": "^2.0.0", + "bin-wrapper": "^3.0.0", + "logalot": "^2.0.0" + } + }, + "orchestrator": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/orchestrator/-/orchestrator-0.3.8.tgz", + "integrity": "sha1-FOfp4nZPcxX7rBhOUGx6pt+UrX4=", + "dev": true, + "requires": { + "end-of-stream": "~0.1.5", + "sequencify": "~0.0.7", + "stream-consume": "~0.1.0" + }, + "dependencies": { + "end-of-stream": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-0.1.5.tgz", + "integrity": "sha1-jhdyBsPICDfYVjLouTWd/osvbq8=", + "dev": true, + "requires": { + "once": "~1.3.0" + } + }, + "once": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", + "integrity": "sha1-suJhVXzkwxTsgwTz+oJmPkKXyiA=", + "dev": true, + "requires": { + "wrappy": "1" + } + } + } + }, + "ordered-read-streams": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-0.3.0.tgz", + "integrity": "sha1-cTfmmzKYuzQiR6G77jiByA4v14s=", + "dev": true, + "requires": { + "is-stream": "^1.0.1", + "readable-stream": "^2.0.1" + } + }, + "os-browserify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.1.2.tgz", + "integrity": "sha1-ScoCk+CxlZCl9d4Qx/JlphfY/lQ=", + "dev": true + }, + "os-filter-obj": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/os-filter-obj/-/os-filter-obj-1.0.3.tgz", + "integrity": "sha1-WRUzDZDs7VV9LZOKMcbdIU2cY60=", + "dev": true, + "optional": true + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "dev": true, + "requires": { + "lcid": "^1.0.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "dev": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "outpipe": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/outpipe/-/outpipe-1.1.1.tgz", + "integrity": "sha1-UM+GFjZeh+Ax4ppeyTOaPaRyX6I=", + "dev": true, + "requires": { + "shell-quote": "^1.4.2" + } + }, + "p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", + "dev": true + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", + "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", + "dev": true + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "package": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package/-/package-1.0.1.tgz", + "integrity": "sha1-0lofmeJQbcsn1nBLg9yooxLk7cw=", + "dev": true + }, + "package-json": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", + "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=", + "dev": true, + "requires": { + "got": "^6.7.1", + "registry-auth-token": "^3.0.1", + "registry-url": "^3.0.3", + "semver": "^5.1.0" + }, + "dependencies": { + "got": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", + "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", + "dev": true, + "requires": { + "create-error-class": "^3.0.0", + "duplexer3": "^0.1.4", + "get-stream": "^3.0.0", + "is-redirect": "^1.0.0", + "is-retry-allowed": "^1.0.0", + "is-stream": "^1.0.0", + "lowercase-keys": "^1.0.0", + "safe-buffer": "^5.0.1", + "timed-out": "^4.0.0", + "unzip-response": "^2.0.1", + "url-parse-lax": "^1.0.0" + } + }, + "timed-out": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", + "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", + "dev": true + }, + "unzip-response": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", + "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=", + "dev": true + } + } + }, + "pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=", + "dev": true + }, + "parallel-transform": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.1.0.tgz", + "integrity": "sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY=", + "dev": true, + "requires": { + "cyclist": "~0.2.2", + "inherits": "^2.0.3", + "readable-stream": "^2.1.5" + } + }, + "param-case": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-1.1.2.tgz", + "integrity": "sha1-3LCRpDwlm5Io8cNB57akTqC/l0M=", + "dev": true, + "requires": { + "sentence-case": "^1.1.2" + } + }, + "parents": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parents/-/parents-1.0.1.tgz", + "integrity": "sha1-/t1NK/GTp3dF/nHjcdc8MwfZx1E=", + "dev": true, + "requires": { + "path-platform": "~0.11.15" + } + }, + "parse-asn1": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.4.tgz", + "integrity": "sha512-Qs5duJcuvNExRfFZ99HDD3z4mAi3r9Wl/FOjEOijlxwCZs7E7mW2vjTpgQ4J8LpTF8x5v+1Vn5UQFejmWT11aw==", + "dev": true, + "requires": { + "asn1.js": "^4.0.0", + "browserify-aes": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.0", + "pbkdf2": "^3.0.3", + "safe-buffer": "^5.1.1" + } + }, + "parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "dev": true, + "requires": { + "glob-base": "^0.3.0", + "is-dotfile": "^1.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.0" + }, + "dependencies": { + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "^1.0.0" + } + } + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "^1.2.0" + } + }, + "parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true + }, + "parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", + "dev": true + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true + }, + "pascal-case": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-1.1.2.tgz", + "integrity": "sha1-Pl1kogBDgwp8STRMLXS0G+DJyZs=", + "dev": true, + "requires": { + "camel-case": "^1.1.1", + "upper-case-first": "^1.1.0" + } + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true + }, + "patch-text": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/patch-text/-/patch-text-1.0.2.tgz", + "integrity": "sha1-S/NuZeUXM9bpjwz2LgkDTaoDSKw=", + "dev": true + }, + "path-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", + "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==", + "dev": true + }, + "path-case": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/path-case/-/path-case-1.1.2.tgz", + "integrity": "sha1-UM5roNO+090LXCqcRVNpdDRAlRQ=", + "dev": true, + "requires": { + "sentence-case": "^1.1.2" + } + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "path-platform": { + "version": "0.11.15", + "resolved": "https://registry.npmjs.org/path-platform/-/path-platform-0.11.15.tgz", + "integrity": "sha1-6GQhf3TDaFDwhSt43Hv31KVyG/I=", + "dev": true + }, + "path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "dev": true, + "requires": { + "pify": "^2.0.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "pbkdf2": { + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", + "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", + "dev": true, + "requires": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "dev": true + }, + "perfect-scrollbar": { + "version": "git://github.com/CartoDB/perfect-scrollbar.git#f2b66c76ad3718d3c704bd7e1693ea382e44e64d", + "from": "git://github.com/CartoDB/perfect-scrollbar.git#master" + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "dev": true + }, + "phantomjs-prebuilt": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/phantomjs-prebuilt/-/phantomjs-prebuilt-2.1.16.tgz", + "integrity": "sha1-79ISpKOWbTZHaE6ouniFSb4q7+8=", + "dev": true, + "requires": { + "es6-promise": "^4.0.3", + "extract-zip": "^1.6.5", + "fs-extra": "^1.0.0", + "hasha": "^2.2.0", + "kew": "^0.7.0", + "progress": "^1.1.8", + "request": "^2.81.0", + "request-progress": "^2.0.1", + "which": "^1.2.10" + }, + "dependencies": { + "es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "dev": true + }, + "progress": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", + "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", + "dev": true + } + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "pkg-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", + "dev": true, + "requires": { + "find-up": "^2.1.0" + } + }, + "plexer": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/plexer/-/plexer-0.0.3.tgz", + "integrity": "sha1-M00dfNYjJGVBfppmNkkB8pIw9TE=", + "dev": true, + "requires": { + "readable-stream": "^1.0.26-2" + }, + "dependencies": { + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + } + } + }, + "pluralize": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz", + "integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==", + "dev": true + }, + "portscanner": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/portscanner/-/portscanner-1.2.0.tgz", + "integrity": "sha1-sUu9olfRTDEPqcwJaCrwLUCWGAI=", + "dev": true, + "requires": { + "async": "1.5.2" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + } + } + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true + }, + "postcss": { + "version": "5.0.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.0.19.tgz", + "integrity": "sha1-tjQqAdx1uMq36Wiv2pau/Gf4iK8=", + "requires": { + "js-base64": "^2.1.9", + "source-map": "^0.5.1", + "supports-color": "^3.1.2" + } + }, + "postcss-value-parser": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz", + "integrity": "sha1-h/OPnxj3dKSrTIojL1xc6IcqnRU=" + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", + "dev": true + }, + "preserve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", + "dev": true + }, + "pretty-bytes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-3.0.1.tgz", + "integrity": "sha1-J9AAjXeAY6C0gRuzXHnxvV1fvM8=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "pretty-hrtime": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-0.2.2.tgz", + "integrity": "sha1-1P2INR46R0H4Fzr31qS4RvmJXAA=", + "dev": true + }, + "pretty-ms": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-0.1.0.tgz", + "integrity": "sha1-fGnMhmumeU6e7wFo/u6t4Lr6fiI=", + "dev": true + }, + "private": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", + "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", + "dev": true + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", + "dev": true + }, + "promise-polyfill": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-6.1.0.tgz", + "integrity": "sha1-36lpQ+qcEh/KTem1hoyznTRy4Fc=" + }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "dev": true + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, + "psl": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.3.0.tgz", + "integrity": "sha512-avHdspHO+9rQTLbv1RO+MPYeP/SzsCoxofjVnHanETfQhTJrmB0HlDoW+EiN/R+C0BZ+gERab9NY0lPN2TxNag==", + "dev": true + }, + "pstree.remy": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.7.tgz", + "integrity": "sha512-xsMgrUwRpuGskEzBFkH8NmTimbZ5PcPup0LA8JJkHIm2IMUbQcpo3yeLNWVrufEYjh8YwtSVh0xz6UeWc5Oh5A==", + "dev": true + }, + "public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "dev": true, + "requires": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + } + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "dev": true, + "optional": true + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "dev": true + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "dev": true + }, + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", + "dev": true + }, + "randomatic": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz", + "integrity": "sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==", + "dev": true, + "requires": { + "is-number": "^4.0.0", + "kind-of": "^6.0.0", + "math-random": "^1.0.1" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "dev": true + } + } + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "dev": true, + "requires": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true + }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "read-all-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/read-all-stream/-/read-all-stream-3.1.0.tgz", + "integrity": "sha1-NcPhd/IHjveJ7kv6+kNzB06u9Po=", + "dev": true, + "optional": true, + "requires": { + "pinkie-promise": "^2.0.0", + "readable-stream": "^2.0.0" + } + }, + "read-only-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-only-stream/-/read-only-stream-2.0.0.tgz", + "integrity": "sha1-JyT9aoET1zdkrCiNQ4YnDB2/F/A=", + "dev": true, + "requires": { + "readable-stream": "^2.0.2" + } + }, + "read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "dev": true, + "requires": { + "load-json-file": "^2.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^2.0.0" + } + }, + "read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "dev": true, + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^2.0.0" + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "readable-wrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/readable-wrap/-/readable-wrap-1.0.0.tgz", + "integrity": "sha1-O1ohHGMeEjA6VJkcgGwX564ga/8=", + "dev": true, + "requires": { + "readable-stream": "^1.1.13-1" + }, + "dependencies": { + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + } + } + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + }, + "readline2": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz", + "integrity": "sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "mute-stream": "0.0.5" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "mute-stream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz", + "integrity": "sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=", + "dev": true + } + } + }, + "recursive-readdir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-1.3.0.tgz", + "integrity": "sha1-xuZsmuRz9JKPjmxnoF2A56VlKO8=", + "dev": true, + "requires": { + "minimatch": "0.3.0" + }, + "dependencies": { + "lru-cache": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", + "dev": true + }, + "minimatch": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", + "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", + "dev": true, + "requires": { + "lru-cache": "2", + "sigmund": "~1.0.0" + } + } + } + }, + "redent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "dev": true, + "requires": { + "indent-string": "^2.1.0", + "strip-indent": "^1.0.1" + } + }, + "regenerate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", + "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==", + "dev": true + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", + "dev": true + }, + "regenerator-transform": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.10.1.tgz", + "integrity": "sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q==", + "dev": true, + "requires": { + "babel-runtime": "^6.18.0", + "babel-types": "^6.19.0", + "private": "^0.1.6" + } + }, + "regex-cache": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", + "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", + "dev": true, + "requires": { + "is-equal-shallow": "^0.1.3" + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "regexpu-core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz", + "integrity": "sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA=", + "dev": true, + "requires": { + "regenerate": "^1.2.1", + "regjsgen": "^0.2.0", + "regjsparser": "^0.1.4" + } + }, + "registry-auth-token": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.4.0.tgz", + "integrity": "sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A==", + "dev": true, + "requires": { + "rc": "^1.1.6", + "safe-buffer": "^5.0.1" + } + }, + "registry-url": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", + "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", + "dev": true, + "requires": { + "rc": "^1.0.1" + } + }, + "regjsgen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", + "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=", + "dev": true + }, + "regjsparser": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", + "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", + "dev": true, + "requires": { + "jsesc": "~0.5.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", + "dev": true + } + } + }, + "relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=", + "dev": true + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "rename-function-calls": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/rename-function-calls/-/rename-function-calls-0.1.1.tgz", + "integrity": "sha1-f4M2nAB6MAf2q+MDPM+BaGoQjgE=", + "dev": true, + "requires": { + "detective": "~3.1.0" + }, + "dependencies": { + "detective": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detective/-/detective-3.1.0.tgz", + "integrity": "sha1-d3gkRKt1K4jKG+Lp0KA5Xx2iXu0=", + "dev": true, + "requires": { + "escodegen": "~1.1.0", + "esprima-fb": "3001.1.0-dev-harmony-fb" + } + } + } + }, + "repeat-element": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "dev": true, + "requires": { + "is-finite": "^1.0.0" + } + }, + "replace-ext": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-0.0.1.tgz", + "integrity": "sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=", + "dev": true + }, + "replace-requires": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/replace-requires/-/replace-requires-1.0.4.tgz", + "integrity": "sha1-AUtzMLa54lV7cQQ7ZvsCZgw79mc=", + "dev": true, + "requires": { + "detective": "^4.5.0", + "has-require": "~1.2.1", + "patch-text": "~1.0.2", + "xtend": "~4.0.0" + }, + "dependencies": { + "has-require": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/has-require/-/has-require-1.2.2.tgz", + "integrity": "sha1-khZ1qxMNvZdo/I2o8ajiQt+kF3Q=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.3" + } + } + } + }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "dev": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", + "dev": true + } + } + }, + "request-progress": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-2.0.1.tgz", + "integrity": "sha1-XTa7V5YcZzqlt4jbyBQf3yO0Tgg=", + "dev": true, + "requires": { + "throttleit": "^1.0.0" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", + "dev": true + }, + "require-uncached": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", + "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", + "dev": true, + "requires": { + "caller-path": "^0.1.0", + "resolve-from": "^1.0.0" + } + }, + "requizzle": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.3.tgz", + "integrity": "sha512-YanoyJjykPxGHii0fZP0uUPEXpvqfBDxWV7s6GKAiiOsiqhX6vHNyW3Qzdmqp/iq/ExbhaGbVrjB4ruEVSM4GQ==", + "dev": true, + "requires": { + "lodash": "^4.17.14" + } + }, + "resolve": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz", + "integrity": "sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, + "resolve-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", + "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", + "dev": true, + "requires": { + "resolve-from": "^3.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", + "dev": true + } + } + }, + "resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", + "dev": true, + "requires": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + }, + "dependencies": { + "global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dev": true, + "requires": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + } + } + } + }, + "resolve-from": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", + "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", + "dev": true + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "dev": true, + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + } + }, + "resumer": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/resumer/-/resumer-0.0.0.tgz", + "integrity": "sha1-8ej0YeQGS6Oegq883CqMiT0HZ1k=", + "dev": true, + "requires": { + "through": "~2.3.4" + } + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + }, + "dependencies": { + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "dev": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "run-async": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", + "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "dev": true, + "requires": { + "is-promise": "^2.1.0" + } + }, + "run-queue": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", + "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", + "dev": true, + "requires": { + "aproba": "^1.1.1" + } + }, + "rx-lite": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz", + "integrity": "sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ=", + "dev": true + }, + "rx-lite-aggregates": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz", + "integrity": "sha1-dTuHqJoRyVRnxKwWJsTvxOBcZ74=", + "dev": true, + "requires": { + "rx-lite": "*" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "requires": { + "ret": "~0.1.10" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "sass-graph": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz", + "integrity": "sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=", + "dev": true, + "requires": { + "glob": "^7.0.0", + "lodash": "^4.0.0", + "scss-tokenizer": "^0.2.3", + "yargs": "^7.0.0" + }, + "dependencies": { + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", + "dev": true + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "dev": true, + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + } + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + } + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "^2.0.0" + } + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "requires": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "requires": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "^0.2.0" + } + }, + "yargs": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz", + "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=", + "dev": true, + "requires": { + "camelcase": "^3.0.0", + "cliui": "^3.2.0", + "decamelize": "^1.1.1", + "get-caller-file": "^1.0.1", + "os-locale": "^1.4.0", + "read-pkg-up": "^1.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^1.0.2", + "which-module": "^1.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^5.0.0" + } + } + } + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true + }, + "schema-utils": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz", + "integrity": "sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-keywords": "^3.1.0" + }, + "dependencies": { + "ajv": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", + "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + } + } + }, + "scss-tokenizer": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", + "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=", + "dev": true, + "requires": { + "js-base64": "^2.1.8", + "source-map": "^0.4.2" + }, + "dependencies": { + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, + "seek-bzip": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.5.tgz", + "integrity": "sha1-z+kXyz0nS8/6x5J1ivUxc+sfq9w=", + "dev": true, + "optional": true, + "requires": { + "commander": "~2.8.1" + }, + "dependencies": { + "commander": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", + "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", + "dev": true, + "optional": true, + "requires": { + "graceful-readlink": ">= 1.0.0" + } + } + } + }, + "semver": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", + "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==", + "dev": true + }, + "semver-diff": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", + "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", + "dev": true, + "requires": { + "semver": "^5.0.3" + } + }, + "semver-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-1.0.0.tgz", + "integrity": "sha1-kqSWkGX5xwxpR1PVUkj8aPj2Usk=", + "dev": true, + "optional": true + }, + "semver-truncate": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/semver-truncate/-/semver-truncate-1.1.2.tgz", + "integrity": "sha1-V/Qd5pcHpicJp+AQS6IRcQnqR+g=", + "dev": true, + "optional": true, + "requires": { + "semver": "^5.3.0" + } + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "dev": true, + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", + "dev": true + } + } + }, + "sentence-case": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-1.1.3.tgz", + "integrity": "sha1-gDSq/CFFdy06vhUJqkLJ4QQtwTk=", + "dev": true, + "requires": { + "lower-case": "^1.1.1" + } + }, + "sequencify": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/sequencify/-/sequencify-0.0.7.tgz", + "integrity": "sha1-kM/xnQLgcCf9dn9erT57ldHnOAw=", + "dev": true + }, + "serialize-javascript": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.7.0.tgz", + "integrity": "sha512-ke8UG8ulpFOxO8f8gRYabHQe/ZntKlcig2Mp+8+URDP1D8vJZ0KUt7LYo07q25Z/+JVSgpr/cui9PIp5H6/+nA==", + "dev": true + }, + "serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", + "dev": true, + "requires": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "dev": true, + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", + "dev": true + }, + "set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", + "dev": true + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "shasum": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/shasum/-/shasum-1.0.2.tgz", + "integrity": "sha1-5wEjENj0F/TetXEhUOVni4euVl8=", + "dev": true, + "requires": { + "json-stable-stringify": "~0.0.0", + "sha.js": "~2.4.4" + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "shell-quote": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz", + "integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=", + "dev": true, + "requires": { + "array-filter": "~0.0.0", + "array-map": "~0.0.0", + "array-reduce": "~0.0.0", + "jsonify": "~0.0.0" + } + }, + "sigmund": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "simple-concat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz", + "integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=", + "dev": true + }, + "simple-statistics": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/simple-statistics/-/simple-statistics-0.9.2.tgz", + "integrity": "sha1-PjXLEDCPx2ljqk7nJS5qbqENKOQ=" + }, + "slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", + "dev": true + }, + "slice-ansi": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-1.0.0.tgz", + "integrity": "sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0" + } + }, + "snake-case": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-1.1.2.tgz", + "integrity": "sha1-DC8l4wUVjZoY09l3BmGH/vilpmo=", + "dev": true, + "requires": { + "sentence-case": "^1.1.2" + } + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "requires": { + "kind-of": "^3.2.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "source-map-resolve": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", + "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", + "dev": true, + "requires": { + "atob": "^2.1.1", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-support": { + "version": "github:CartoDB/node-source-map-support#dcf408d55396d7b18ada0f525d477ac1f4af2242", + "from": "github:CartoDB/node-source-map-support#0.4.6-cdb1", + "dev": true, + "requires": { + "source-map": "^0.5.3" + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true + }, + "sparkles": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.1.tgz", + "integrity": "sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw==", + "dev": true + }, + "spdx-correct": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", + "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", + "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", + "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", + "dev": true + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "squeak": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/squeak/-/squeak-1.3.0.tgz", + "integrity": "sha1-MwRQN7ZDiLVnZ0uEMiplIQc5FsM=", + "dev": true, + "optional": true, + "requires": { + "chalk": "^1.0.0", + "console-stream": "^0.1.1", + "lpad-align": "^1.0.1" + } + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "dev": true, + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "ssri": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-5.3.0.tgz", + "integrity": "sha512-XRSIPqLij52MtgoQavH/x/dU1qVKtWUAAZeOHsR9c2Ddi4XerFy3mc1alf+dLJKl9EUIm/Ht+EowFkTUOA6GAQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.1" + } + }, + "stat-mode": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-0.2.2.tgz", + "integrity": "sha1-5sgLYjEj19gM8TLOU480YokHJQI=", + "dev": true, + "optional": true + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "dev": true + }, + "stdout-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz", + "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==", + "dev": true, + "requires": { + "readable-stream": "^2.0.1" + } + }, + "stream-browserify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", + "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", + "dev": true, + "requires": { + "inherits": "~2.0.1", + "readable-stream": "^2.0.2" + } + }, + "stream-combiner2": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", + "integrity": "sha1-+02KFCDqNidk4hrUeAOXvry0HL4=", + "dev": true, + "requires": { + "duplexer2": "~0.1.0", + "readable-stream": "^2.0.2" + } + }, + "stream-consume": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/stream-consume/-/stream-consume-0.1.1.tgz", + "integrity": "sha512-tNa3hzgkjEP7XbCkbRXe1jpg+ievoa0O4SCFlMOYEscGSS4JJsckGL8swUyAa/ApGU3Ae4t6Honor4HhL+tRyg==", + "dev": true + }, + "stream-each": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", + "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "stream-shift": "^1.0.0" + } + }, + "stream-http": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", + "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", + "dev": true, + "requires": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.3.6", + "to-arraybuffer": "^1.0.0", + "xtend": "^4.0.0" + } + }, + "stream-shift": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", + "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=", + "dev": true + }, + "stream-splicer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/stream-splicer/-/stream-splicer-2.0.1.tgz", + "integrity": "sha512-Xizh4/NPuYSyAXyT7g8IvdJ9HJpxIGL9PjyhtywCZvvP0OPIdqyrr4dMikeuvY8xahpdKEBlBTySe583totajg==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.2" + } + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-bom-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-stream/-/strip-bom-stream-1.0.0.tgz", + "integrity": "sha1-5xRDmFd9Uaa+0PoZlPoF9D/ZiO4=", + "dev": true, + "requires": { + "first-chunk-stream": "^1.0.0", + "strip-bom": "^2.0.0" + }, + "dependencies": { + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "^0.2.0" + } + } + } + }, + "strip-dirs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-1.1.1.tgz", + "integrity": "sha1-lgu9EoeETzl1pFWKoQOoJV4kVqA=", + "dev": true, + "optional": true, + "requires": { + "chalk": "^1.0.0", + "get-stdin": "^4.0.1", + "is-absolute": "^0.1.5", + "is-natural-number": "^2.0.0", + "minimist": "^1.1.0", + "sum-up": "^1.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true, + "optional": true + } + } + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, + "strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "dev": true, + "requires": { + "get-stdin": "^4.0.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, + "strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "dev": true, + "optional": true, + "requires": { + "escape-string-regexp": "^1.0.2" + } + }, + "subarg": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", + "integrity": "sha1-9izxdYHplrSPyWVpn1TAauJouNI=", + "dev": true, + "requires": { + "minimist": "^1.1.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "sum-up": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sum-up/-/sum-up-1.0.3.tgz", + "integrity": "sha1-HGYfZnBX9jvLeHWqFDi8FiUlFW4=", + "dev": true, + "optional": true, + "requires": { + "chalk": "^1.0.0" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "requires": { + "has-flag": "^1.0.0" + } + }, + "svg-pathdata": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-1.0.0.tgz", + "integrity": "sha1-kPahyWPNS+E6njAPeaGj3ePIAzQ=", + "dev": true, + "requires": { + "readable-stream": "~1.0.26-3" + }, + "dependencies": { + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + } + } + }, + "svg2ttf": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/svg2ttf/-/svg2ttf-1.2.0.tgz", + "integrity": "sha1-dgEvHXb9rzfDYm6ydZx0x6Zj4kI=", + "dev": true, + "requires": { + "argparse": "~ 0.1.15", + "lodash": "~ 2.1.0", + "svgpath": "~ 1.0.0", + "xmldom": "~ 0.1.16" + }, + "dependencies": { + "argparse": { + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-0.1.16.tgz", + "integrity": "sha1-z9AeD7uj1srtBJ+9dY1A9lGW9Xw=", + "dev": true, + "requires": { + "underscore": "~1.7.0", + "underscore.string": "~2.4.0" + } + }, + "lodash": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.1.0.tgz", + "integrity": "sha1-Bjfqqjaooc/IZcOt+5Qhib+wmY0=", + "dev": true + }, + "underscore": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", + "integrity": "sha1-a7rwh3UA02vjTsqlhODbn+8DUgk=", + "dev": true + }, + "underscore.string": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.4.0.tgz", + "integrity": "sha1-jN2PusTi0uoefi6Al8QvRCKA+Fs=", + "dev": true + } + } + }, + "svgicons2svgfont": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/svgicons2svgfont/-/svgicons2svgfont-1.0.0.tgz", + "integrity": "sha1-POxfPzLtbUjOgSR7BI9CIkCaac0=", + "dev": true, + "requires": { + "readable-stream": "^1.0.33", + "sax": "0.6.x", + "svg-pathdata": "1.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "sax": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-0.6.1.tgz", + "integrity": "sha1-VjsZx8HeiS4Jv8Ty/DDjwn8JUrk=", + "dev": true + } + } + }, + "svgo": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-0.6.6.tgz", + "integrity": "sha1-s0CIkDbyD5tEdUMHfQ9Vc+0ETAg=", + "dev": true, + "optional": true, + "requires": { + "coa": "~1.0.1", + "colors": "~1.1.2", + "csso": "~2.0.0", + "js-yaml": "~3.6.0", + "mkdirp": "~0.5.1", + "sax": "~1.2.1", + "whet.extend": "~0.9.9" + }, + "dependencies": { + "colors": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", + "dev": true, + "optional": true + }, + "esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", + "dev": true, + "optional": true + }, + "js-yaml": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.6.1.tgz", + "integrity": "sha1-bl/mfYsgXOTSL60Ft3geja3MSzA=", + "dev": true, + "optional": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^2.6.0" + } + } + } + }, + "svgpath": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/svgpath/-/svgpath-1.0.7.tgz", + "integrity": "sha1-5VQhK4fl0EaRQaKEwCDIf1yaTtA=", + "dev": true + }, + "swap-case": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/swap-case/-/swap-case-1.1.2.tgz", + "integrity": "sha1-w5IDpFhzhfrTyFCgvRvK+ggZdOM=", + "dev": true, + "requires": { + "lower-case": "^1.1.1", + "upper-case": "^1.1.1" + } + }, + "syntax-error": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.4.0.tgz", + "integrity": "sha512-YPPlu67mdnHGTup2A8ff7BC2Pjq0e0Yp/IyTFN03zWO0RcK07uLcbi7C2KpGR2FvWbaB0+bfE27a+sBKebSo7w==", + "dev": true, + "requires": { + "acorn-node": "^1.2.0" + } + }, + "table": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/table/-/table-4.0.2.tgz", + "integrity": "sha512-UUkEAPdSGxtRpiV9ozJ5cMTtYiqz7Ni1OGqLXRCynrvzdtR1p+cfOWe2RJLwvUG8hNanaSRjecIqwOjqeatDsA==", + "dev": true, + "requires": { + "ajv": "^5.2.3", + "ajv-keywords": "^2.1.0", + "chalk": "^2.1.0", + "lodash": "^4.17.4", + "slice-ansi": "1.0.0", + "string-width": "^2.1.1" + }, + "dependencies": { + "ajv-keywords": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-2.1.1.tgz", + "integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "taffydb": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/taffydb/-/taffydb-2.6.2.tgz", + "integrity": "sha1-fLy2S1oUG2ou/CxdLGe04VCyomg=", + "dev": true + }, + "tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "dev": true + }, + "tar": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz", + "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==", + "dev": true, + "requires": { + "block-stream": "*", + "fstream": "^1.0.12", + "inherits": "2" + } + }, + "tar-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", + "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", + "dev": true, + "optional": true, + "requires": { + "bl": "^1.0.0", + "buffer-alloc": "^1.2.0", + "end-of-stream": "^1.0.0", + "fs-constants": "^1.0.0", + "readable-stream": "^2.3.0", + "to-buffer": "^1.1.1", + "xtend": "^4.0.0" + } + }, + "tempfile": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tempfile/-/tempfile-1.1.1.tgz", + "integrity": "sha1-W8xOrsxKsscH2LwR2ZzMmiyyh/I=", + "dev": true, + "optional": true, + "requires": { + "os-tmpdir": "^1.0.0", + "uuid": "^2.0.1" + } + }, + "temporary": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/temporary/-/temporary-0.0.8.tgz", + "integrity": "sha1-oYqYHSi6jKNgJ/s8MFOMPst0CsA=", + "dev": true, + "requires": { + "package": ">= 1.0.0 < 1.2.0" + } + }, + "term-size": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", + "integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=", + "dev": true, + "requires": { + "execa": "^0.7.0" + } + }, + "ternary": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ternary/-/ternary-1.0.0.tgz", + "integrity": "sha1-RXAnJWCMlJnUapYQ6bDkn/JveJ4=", + "dev": true + }, + "terser": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.4.0.tgz", + "integrity": "sha512-oDG16n2WKm27JO8h4y/w3iqBGAOSCtq7k8dRmrn4Wf9NouL0b2WpMHGChFGZq4nFAQy1FsNJrVQHfurXOSTmOA==", + "dev": true, + "requires": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-support": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz", + "integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + } + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "throttleit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", + "integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "through2-filter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-2.0.0.tgz", + "integrity": "sha1-YLxVoNrLdghdsfna6Zq0P4PWIuw=", + "dev": true, + "requires": { + "through2": "~2.0.0", + "xtend": "~4.0.0" + } + }, + "tildify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tildify/-/tildify-1.2.0.tgz", + "integrity": "sha1-3OwD9V3Km3qj5bBPIYF+tW5jWIo=", + "dev": true, + "requires": { + "os-homedir": "^1.0.0" + } + }, + "time-grunt": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/time-grunt/-/time-grunt-0.3.2.tgz", + "integrity": "sha1-8wE2RbAeaOJ4AqPkxHAs7KC9/68=", + "dev": true, + "requires": { + "chalk": "^0.4.0", + "date-time": "^0.1.0", + "hooker": "^0.2.3", + "pretty-ms": "^0.1.0", + "text-table": "^0.2.0" + }, + "dependencies": { + "ansi-styles": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.0.0.tgz", + "integrity": "sha1-yxAt8cVvUSPquLZ817mAJ6AnkXg=", + "dev": true + }, + "chalk": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.4.0.tgz", + "integrity": "sha1-UZmj3c0MHv4jvAjBsCewYXbgxk8=", + "dev": true, + "requires": { + "ansi-styles": "~1.0.0", + "has-color": "~0.1.0", + "strip-ansi": "~0.1.0" + } + }, + "strip-ansi": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.1.1.tgz", + "integrity": "sha1-OeipjQRNFQZgq+SmgIrPcLt7yZE=", + "dev": true + } + } + }, + "time-stamp": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz", + "integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=", + "dev": true + }, + "timed-out": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-3.1.3.tgz", + "integrity": "sha1-lYYL/MXHbCd/j4Mm/Q9bLiDrohc=", + "dev": true, + "optional": true + }, + "timers-browserify": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-1.4.2.tgz", + "integrity": "sha1-ycWLV1voQHN1y14kYtrO50NZ9B0=", + "dev": true, + "requires": { + "process": "~0.11.0" + } + }, + "tiny-lr": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/tiny-lr/-/tiny-lr-0.0.5.tgz", + "integrity": "sha1-02eSJh0+rfxq7UkpcrPhNyt/yCk=", + "dev": true, + "requires": { + "debug": "~0.7.0", + "faye-websocket": "~0.4.3", + "noptify": "^0.0.3", + "qs": "~0.5.2" + }, + "dependencies": { + "debug": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz", + "integrity": "sha1-BuHqgILCyxTjmAbiLi9vdX+Srzk=", + "dev": true + }, + "qs": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/qs/-/qs-0.5.6.tgz", + "integrity": "sha1-MbGtBYVnZRxSaSFQa5qHk5EaA4Q=", + "dev": true + } + } + }, + "title-case": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/title-case/-/title-case-1.1.2.tgz", + "integrity": "sha1-+uSmrlRr+iLQg6DuqRCkDRLtT1o=", + "dev": true, + "requires": { + "sentence-case": "^1.1.1", + "upper-case": "^1.0.3" + } + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, + "to-absolute-glob": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-0.1.1.tgz", + "integrity": "sha1-HN+kcqnvUMI57maZm2YsoOs5k38=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", + "dev": true + }, + "to-buffer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", + "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==", + "dev": true, + "optional": true + }, + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", + "dev": true + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "dev": true + }, + "torque.js": { + "version": "github:CartoDB/torque#11b73bbc9a55b7c67c1ab72759a58eb417b88555", + "from": "github:CartoDB/torque#master", + "requires": { + "carto": "github:cartodb/carto#master", + "d3": "3.5.17", + "turbo-carto": "^0.21.1", + "turf-jenks": "~1.0.1" + }, + "dependencies": { + "carto": { + "version": "github:cartodb/carto#85881d99dd7fcf2c4e16478b04db67108d27a50c", + "from": "github:cartodb/carto#master", + "requires": { + "mapnik-reference": "~6.0.2", + "optimist": "~0.6.0", + "underscore": "1.8.3" + } + } + } + }, + "touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, + "requires": { + "nopt": "~1.0.10" + } + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "dev": true, + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + } + }, + "transformify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/transformify/-/transformify-0.1.2.tgz", + "integrity": "sha1-mk9CoVRDPdcnuAV1Qoo8nlSJ6/E=", + "dev": true, + "requires": { + "readable-stream": "~1.1.9" + }, + "dependencies": { + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + } + } + }, + "trim-newlines": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", + "dev": true + }, + "trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha1-42RqLqTokTEr9+rObPsFOAvAHCE=", + "dev": true, + "optional": true, + "requires": { + "escape-string-regexp": "^1.0.2" + } + }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", + "dev": true + }, + "true-case-path": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz", + "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==", + "dev": true, + "requires": { + "glob": "^7.1.2" + }, + "dependencies": { + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "tslib": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", + "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", + "dev": true + }, + "ttf2eot": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ttf2eot/-/ttf2eot-2.0.0.tgz", + "integrity": "sha1-jmM3pYWr0WCKDISVirSDzmn2ZUs=", + "dev": true, + "requires": { + "argparse": "^1.0.6", + "microbuffer": "^1.0.0" + } + }, + "ttf2woff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ttf2woff/-/ttf2woff-2.0.1.tgz", + "integrity": "sha1-hxgyJAAksJ25VwkEx8GSi4BXyWk=", + "dev": true, + "requires": { + "argparse": "^1.0.6", + "microbuffer": "^1.0.0", + "pako": "^1.0.0" + }, + "dependencies": { + "pako": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz", + "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==", + "dev": true + } + } + }, + "tty-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", + "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==", + "dev": true + }, + "tunnel-agent": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", + "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=", + "dev": true, + "optional": true + }, + "turbo-carto": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/turbo-carto/-/turbo-carto-0.21.2.tgz", + "integrity": "sha512-yUqc5V9U5UEVksMfNSlDOr03mHEQBa187iTM4nbTiOwViBVAo/ndW07INt2np9gin+YO8MFJjo7Ioo8lr+3xUQ==", + "requires": { + "cartocolor": "4.0.0", + "colorbrewer": "1.0.0", + "debug": "^3.1.0", + "es6-promise": "3.1.2", + "postcss": "5.0.19", + "postcss-value-parser": "3.3.0" + } + }, + "turf-jenks": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/turf-jenks/-/turf-jenks-1.0.1.tgz", + "integrity": "sha1-XHKuoTpxbv8vACOnLPpUmgAgLX8=", + "requires": { + "simple-statistics": "^0.9.0" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "dev": true + }, + "uglifyjs-webpack-plugin": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.3.0.tgz", + "integrity": "sha512-ovHIch0AMlxjD/97j9AYovZxG5wnHOPkL7T1GKochBADp/Zwc44pEWNqpKl1Loupp1WhFg7SlYmHZRUfdAacgw==", + "dev": true, + "requires": { + "cacache": "^10.0.4", + "find-cache-dir": "^1.0.0", + "schema-utils": "^0.4.5", + "serialize-javascript": "^1.4.0", + "source-map": "^0.6.1", + "uglify-es": "^3.3.4", + "webpack-sources": "^1.1.0", + "worker-farm": "^1.5.2" + }, + "dependencies": { + "commander": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz", + "integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "uglify-es": { + "version": "3.3.9", + "resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz", + "integrity": "sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ==", + "dev": true, + "requires": { + "commander": "~2.13.0", + "source-map": "~0.6.1" + } + } + } + }, + "umd": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/umd/-/umd-3.0.3.tgz", + "integrity": "sha512-4IcGSufhFshvLNcMCV80UnQVlZ5pMOC8mvNPForqwA4+lzYQuetTESLDQkeLmihq8bRcnpbQa48Wb8Lh16/xow==", + "dev": true + }, + "undeclared-identifiers": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/undeclared-identifiers/-/undeclared-identifiers-1.1.3.tgz", + "integrity": "sha512-pJOW4nxjlmfwKApE4zvxLScM/njmwj/DiUBv7EabwE4O8kRUy+HIwxQtZLBPll/jx1LJyBcqNfB3/cpv9EZwOw==", + "dev": true, + "requires": { + "acorn-node": "^1.3.0", + "dash-ast": "^1.0.0", + "get-assigned-identifiers": "^1.2.0", + "simple-concat": "^1.0.0", + "xtend": "^4.0.1" + } + }, + "undefsafe": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.2.tgz", + "integrity": "sha1-Il9rngM3Zj4Njnz9aG/Cg2zKznY=", + "dev": true, + "requires": { + "debug": "^2.2.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "underscore": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", + "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=" + }, + "underscore.string": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.2.1.tgz", + "integrity": "sha1-18D6KvXVoaZ/QlPa7pgTLnM/Dxk=", + "dev": true + }, + "union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + } + }, + "unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "dev": true, + "requires": { + "unique-slug": "^2.0.0" + } + }, + "unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4" + } + }, + "unique-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.3.1.tgz", + "integrity": "sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A==", + "dev": true, + "requires": { + "json-stable-stringify-without-jsonify": "^1.0.1", + "through2-filter": "^3.0.0" + }, + "dependencies": { + "through2-filter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-3.0.0.tgz", + "integrity": "sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA==", + "dev": true, + "requires": { + "through2": "~2.0.0", + "xtend": "~4.0.0" + } + } + } + }, + "unique-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", + "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", + "dev": true, + "requires": { + "crypto-random-string": "^1.0.0" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "dev": true + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + } + } + }, + "unzip-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-1.0.2.tgz", + "integrity": "sha1-uYTwh3/AqJwsdzzB73tbIytbBv4=", + "dev": true, + "optional": true + }, + "upath": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.2.tgz", + "integrity": "sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q==", + "dev": true + }, + "update-notifier": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-2.5.0.tgz", + "integrity": "sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==", + "dev": true, + "requires": { + "boxen": "^1.2.1", + "chalk": "^2.0.1", + "configstore": "^3.0.0", + "import-lazy": "^2.1.0", + "is-ci": "^1.0.10", + "is-installed-globally": "^0.1.0", + "is-npm": "^1.0.0", + "latest-version": "^3.0.0", + "semver-diff": "^2.0.0", + "xdg-basedir": "^3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "upper-case": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", + "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=", + "dev": true + }, + "upper-case-first": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-1.1.2.tgz", + "integrity": "sha1-XXm+3P8UQZUY/S7bCgUHybaFkRU=", + "dev": true, + "requires": { + "upper-case": "^1.1.1" + } + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + } + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + } + } + }, + "url-parse-lax": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", + "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", + "dev": true, + "requires": { + "prepend-http": "^1.0.1" + } + }, + "url-regex": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/url-regex/-/url-regex-3.2.0.tgz", + "integrity": "sha1-260eDJ4p4QXdCx8J9oYvf9tIJyQ=", + "dev": true, + "optional": true, + "requires": { + "ip-regex": "^1.0.1" + } + }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true + }, + "util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "dev": true, + "requires": { + "inherits": "2.0.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + } + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "dev": true + }, + "uuid": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz", + "integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=", + "dev": true + }, + "v8-compile-cache": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz", + "integrity": "sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w==", + "dev": true + }, + "v8flags": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-1.0.8.tgz", + "integrity": "sha1-fnqmEZyC5Psjk84f/Shos1zZEIQ=", + "dev": true + }, + "vali-date": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/vali-date/-/vali-date-1.0.0.tgz", + "integrity": "sha1-G5BKWWCfsyjvB4E4Qgk09rhnCaY=", + "dev": true + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "vinyl": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", + "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=", + "dev": true, + "requires": { + "clone": "^1.0.0", + "clone-stats": "^0.0.1", + "replace-ext": "0.0.1" + } + }, + "vinyl-assign": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/vinyl-assign/-/vinyl-assign-1.2.1.tgz", + "integrity": "sha1-TRmIkbVRWRHXcajNnFSApGoHSkU=", + "dev": true, + "optional": true, + "requires": { + "object-assign": "^4.0.1", + "readable-stream": "^2.0.0" + } + }, + "vinyl-fs": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-2.4.4.tgz", + "integrity": "sha1-vm/zJwy1Xf19MGNkDegfJddTIjk=", + "dev": true, + "requires": { + "duplexify": "^3.2.0", + "glob-stream": "^5.3.2", + "graceful-fs": "^4.0.0", + "gulp-sourcemaps": "1.6.0", + "is-valid-glob": "^0.3.0", + "lazystream": "^1.0.0", + "lodash.isequal": "^4.0.0", + "merge-stream": "^1.0.0", + "mkdirp": "^0.5.0", + "object-assign": "^4.0.0", + "readable-stream": "^2.0.4", + "strip-bom": "^2.0.0", + "strip-bom-stream": "^1.0.0", + "through2": "^2.0.0", + "through2-filter": "^2.0.0", + "vali-date": "^1.0.0", + "vinyl": "^1.0.0" + }, + "dependencies": { + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "^0.2.0" + } + } + } + }, + "vm-browserify": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", + "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=", + "dev": true, + "requires": { + "indexof": "0.0.1" + } + }, + "ware": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ware/-/ware-1.3.0.tgz", + "integrity": "sha1-0bFPOdLiy0q4xAmPdW/ksWTkc9Q=", + "dev": true, + "optional": true, + "requires": { + "wrap-fn": "^0.1.0" + } + }, + "watchify": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/watchify/-/watchify-3.4.0.tgz", + "integrity": "sha1-RsItAZe8tDEPZzJeoXZoQNBD+Fs=", + "dev": true, + "requires": { + "anymatch": "^1.3.0", + "browserify": "^11.0.1", + "chokidar": "^1.0.0", + "defined": "^1.0.0", + "outpipe": "^1.1.0", + "through2": "~0.6.3", + "xtend": "^4.0.0" + }, + "dependencies": { + "anymatch": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", + "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", + "dev": true, + "requires": { + "micromatch": "^2.1.5", + "normalize-path": "^2.0.0" + } + }, + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "dev": true, + "requires": { + "arr-flatten": "^1.0.1" + } + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "dev": true + }, + "base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha1-EQHpVE9KdrG8OybUUsqW16NeeXg=", + "dev": true + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "dev": true, + "requires": { + "expand-range": "^1.8.1", + "preserve": "^0.2.0", + "repeat-element": "^1.1.2" + } + }, + "browser-pack": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/browser-pack/-/browser-pack-5.0.1.tgz", + "integrity": "sha1-QZdxmyDG4KqglFHFER5T77b7wY0=", + "dev": true, + "requires": { + "JSONStream": "^1.0.3", + "combine-source-map": "~0.6.1", + "defined": "^1.0.0", + "through2": "^1.0.0", + "umd": "^3.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "through2": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/through2/-/through2-1.1.1.tgz", + "integrity": "sha1-CEfLxESfNAVXTb3M2buEG4OsNUU=", + "dev": true, + "requires": { + "readable-stream": ">=1.1.13-1 <1.2.0-0", + "xtend": ">=4.0.0 <4.1.0-0" + } + } + } + }, + "browserify": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/browserify/-/browserify-11.2.0.tgz", + "integrity": "sha1-oRu53SCdeVcrgT9+7q+Cil9cDk4=", + "dev": true, + "requires": { + "JSONStream": "^1.0.3", + "assert": "~1.3.0", + "browser-pack": "^5.0.0", + "browser-resolve": "^1.7.1", + "browserify-zlib": "~0.1.2", + "buffer": "^3.0.0", + "builtins": "~0.0.3", + "commondir": "0.0.1", + "concat-stream": "~1.4.1", + "console-browserify": "^1.1.0", + "constants-browserify": "~0.0.1", + "crypto-browserify": "^3.0.0", + "defined": "^1.0.0", + "deps-sort": "^1.3.7", + "domain-browser": "~1.1.0", + "duplexer2": "~0.0.2", + "events": "~1.0.0", + "glob": "^4.0.5", + "has": "^1.0.0", + "htmlescape": "^1.1.0", + "https-browserify": "~0.0.0", + "inherits": "~2.0.1", + "insert-module-globals": "^6.4.1", + "isarray": "0.0.1", + "labeled-stream-splicer": "^1.0.0", + "module-deps": "^3.7.11", + "os-browserify": "~0.1.1", + "parents": "^1.0.1", + "path-browserify": "~0.0.0", + "process": "~0.11.0", + "punycode": "^1.3.2", + "querystring-es3": "~0.2.0", + "read-only-stream": "^1.1.1", + "readable-stream": "^2.0.2", + "resolve": "^1.1.4", + "shasum": "^1.0.0", + "shell-quote": "~0.0.1", + "stream-browserify": "^2.0.0", + "stream-http": "^1.2.0", + "string_decoder": "~0.10.0", + "subarg": "^1.0.0", + "syntax-error": "^1.1.1", + "through2": "^1.0.0", + "timers-browserify": "^1.0.1", + "tty-browserify": "~0.0.0", + "url": "~0.10.1", + "util": "~0.10.1", + "vm-browserify": "~0.0.1", + "xtend": "^4.0.0" + }, + "dependencies": { + "through2": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/through2/-/through2-1.1.1.tgz", + "integrity": "sha1-CEfLxESfNAVXTb3M2buEG4OsNUU=", + "dev": true, + "requires": { + "readable-stream": ">=1.1.13-1 <1.2.0-0", + "xtend": ">=4.0.0 <4.1.0-0" + }, + "dependencies": { + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + } + } + } + } + }, + "buffer": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-3.6.0.tgz", + "integrity": "sha1-pyyTb3e5a/UvX357RnGAYoVR3vs=", + "dev": true, + "requires": { + "base64-js": "0.0.8", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + } + } + }, + "builtin-status-codes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-1.0.0.tgz", + "integrity": "sha1-MGN+4mKXisBxdOFtf4LwrQbgha0=", + "dev": true + }, + "chokidar": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", + "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "dev": true, + "requires": { + "anymatch": "^1.3.0", + "async-each": "^1.0.0", + "fsevents": "^1.0.0", + "glob-parent": "^2.0.0", + "inherits": "^2.0.1", + "is-binary-path": "^1.0.0", + "is-glob": "^2.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.0.0" + } + }, + "combine-source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.6.1.tgz", + "integrity": "sha1-m0oJwxYDPXaODxHgKfonMOB5rZY=", + "dev": true, + "requires": { + "convert-source-map": "~1.1.0", + "inline-source-map": "~0.5.0", + "lodash.memoize": "~3.0.3", + "source-map": "~0.4.2" + } + }, + "commondir": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-0.0.1.tgz", + "integrity": "sha1-ifAP3NUbUZxXhzP+xWPmptp/W+I=", + "dev": true + }, + "concat-stream": { + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.4.11.tgz", + "integrity": "sha512-X3JMh8+4je3U1cQpG87+f9lXHDrqcb2MVLg9L7o8b1UZ0DzhRrUpdn65ttzu10PpJPPI3MQNkis+oha6TSA9Mw==", + "dev": true, + "requires": { + "inherits": "~2.0.1", + "readable-stream": "~1.1.9", + "typedarray": "~0.0.5" + }, + "dependencies": { + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + } + } + }, + "constants-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-0.0.1.tgz", + "integrity": "sha1-kld9tSe6bEzwpFaNhLwDH0QeIfI=", + "dev": true + }, + "convert-source-map": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz", + "integrity": "sha1-SCnId+n+SbMWHzvzZziI4gRpmGA=", + "dev": true + }, + "deps-sort": { + "version": "1.3.9", + "resolved": "https://registry.npmjs.org/deps-sort/-/deps-sort-1.3.9.tgz", + "integrity": "sha1-Kd//U+F7Nq7K51MK27v2IsLtGnE=", + "dev": true, + "requires": { + "JSONStream": "^1.0.3", + "shasum": "^1.0.0", + "subarg": "^1.0.0", + "through2": "^1.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "through2": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/through2/-/through2-1.1.1.tgz", + "integrity": "sha1-CEfLxESfNAVXTb3M2buEG4OsNUU=", + "dev": true, + "requires": { + "readable-stream": ">=1.1.13-1 <1.2.0-0", + "xtend": ">=4.0.0 <4.1.0-0" + } + } + } + }, + "duplexer2": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz", + "integrity": "sha1-xhTc9n4vsUmVqRcR5aYX6KYKMds=", + "dev": true, + "requires": { + "readable-stream": "~1.1.9" + }, + "dependencies": { + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + } + } + }, + "events": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/events/-/events-1.0.2.tgz", + "integrity": "sha1-dYSdz+k9EPsFfDAFWv29UdBqjiQ=", + "dev": true + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "dev": true, + "requires": { + "is-posix-bracket": "^0.1.0" + } + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "dev": true, + "requires": { + "is-extglob": "^1.0.0" + } + }, + "glob": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-4.5.3.tgz", + "integrity": "sha1-xstz0yJsHv7wTePFbQEvAzd+4V8=", + "dev": true, + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^2.0.1", + "once": "^1.3.0" + } + }, + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "dev": true, + "requires": { + "is-glob": "^2.0.0" + } + }, + "inline-source-map": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/inline-source-map/-/inline-source-map-0.5.0.tgz", + "integrity": "sha1-Skxd2OT7Xps82mDIIt+tyu5m4K8=", + "dev": true, + "requires": { + "source-map": "~0.4.0" + } + }, + "insert-module-globals": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/insert-module-globals/-/insert-module-globals-6.6.3.tgz", + "integrity": "sha1-IGOOKaMPntHKLjqCX7wsulJG3fw=", + "dev": true, + "requires": { + "JSONStream": "^1.0.3", + "combine-source-map": "~0.6.1", + "concat-stream": "~1.4.1", + "is-buffer": "^1.1.0", + "lexical-scope": "^1.2.0", + "process": "~0.11.0", + "through2": "^1.0.0", + "xtend": "^4.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "through2": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/through2/-/through2-1.1.1.tgz", + "integrity": "sha1-CEfLxESfNAVXTb3M2buEG4OsNUU=", + "dev": true, + "requires": { + "readable-stream": ">=1.1.13-1 <1.2.0-0", + "xtend": ">=4.0.0 <4.1.0-0" + } + } + } + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "^1.0.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + }, + "labeled-stream-splicer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/labeled-stream-splicer/-/labeled-stream-splicer-1.0.2.tgz", + "integrity": "sha1-RhUzFTd4SYHo/SZOHzpDTE4N3WU=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "isarray": "~0.0.1", + "stream-splicer": "^1.1.0" + } + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "dev": true, + "requires": { + "arr-diff": "^2.0.0", + "array-unique": "^0.2.1", + "braces": "^1.8.2", + "expand-brackets": "^0.1.4", + "extglob": "^0.3.1", + "filename-regex": "^2.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.1", + "kind-of": "^3.0.2", + "normalize-path": "^2.0.1", + "object.omit": "^2.0.0", + "parse-glob": "^3.0.4", + "regex-cache": "^0.4.2" + } + }, + "minimatch": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-2.0.10.tgz", + "integrity": "sha1-jQh8OcazjAAbl/ynzm0OHoCvusc=", + "dev": true, + "requires": { + "brace-expansion": "^1.0.0" + } + }, + "module-deps": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-3.9.1.tgz", + "integrity": "sha1-6nXK+RmQkNJbDVUStaysuW5/h/M=", + "dev": true, + "requires": { + "JSONStream": "^1.0.3", + "browser-resolve": "^1.7.0", + "concat-stream": "~1.4.5", + "defined": "^1.0.0", + "detective": "^4.0.0", + "duplexer2": "0.0.2", + "inherits": "^2.0.1", + "parents": "^1.0.0", + "readable-stream": "^1.1.13", + "resolve": "^1.1.3", + "stream-combiner2": "~1.0.0", + "subarg": "^1.0.0", + "through2": "^1.0.0", + "xtend": "^4.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "through2": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/through2/-/through2-1.1.1.tgz", + "integrity": "sha1-CEfLxESfNAVXTb3M2buEG4OsNUU=", + "dev": true, + "requires": { + "readable-stream": ">=1.1.13-1 <1.2.0-0", + "xtend": ">=4.0.0 <4.1.0-0" + } + } + } + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "read-only-stream": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/read-only-stream/-/read-only-stream-1.1.1.tgz", + "integrity": "sha1-Xad8eZ7ROI0++IoYRxu1kk+KC6E=", + "dev": true, + "requires": { + "readable-stream": "^1.0.31", + "readable-wrap": "^1.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + } + } + }, + "shell-quote": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-0.0.1.tgz", + "integrity": "sha1-GkEZbzwDM8SCMjWT1ohuzxU92YY=", + "dev": true + }, + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "requires": { + "amdefine": ">=0.0.4" + } + }, + "stream-combiner2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.0.2.tgz", + "integrity": "sha1-unKmtQy/q/qVD8i8h2BL0B62BnE=", + "dev": true, + "requires": { + "duplexer2": "~0.0.2", + "through2": "~0.5.1" + }, + "dependencies": { + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "through2": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.5.1.tgz", + "integrity": "sha1-390BLrnHAOIyP9M084rGIqs3Lac=", + "dev": true, + "requires": { + "readable-stream": "~1.0.17", + "xtend": "~3.0.0" + } + }, + "xtend": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-3.0.0.tgz", + "integrity": "sha1-XM50B7r2Qsunvs2laBEcST9ZZlo=", + "dev": true + } + } + }, + "stream-http": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-1.7.1.tgz", + "integrity": "sha1-09Km4Uw2o4udr7GZrue7xXBRmXg=", + "dev": true, + "requires": { + "builtin-status-codes": "^1.0.0", + "foreach": "^2.0.5", + "indexof": "0.0.1", + "inherits": "^2.0.1", + "object-keys": "^1.0.4", + "xtend": "^4.0.0" + } + }, + "stream-splicer": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/stream-splicer/-/stream-splicer-1.3.2.tgz", + "integrity": "sha1-PARBvhW5v04iYnXm3IOWR0VUZmE=", + "dev": true, + "requires": { + "indexof": "0.0.1", + "inherits": "^2.0.1", + "isarray": "~0.0.1", + "readable-stream": "^1.1.13-1", + "readable-wrap": "^1.0.0", + "through2": "^1.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "through2": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/through2/-/through2-1.1.1.tgz", + "integrity": "sha1-CEfLxESfNAVXTb3M2buEG4OsNUU=", + "dev": true, + "requires": { + "readable-stream": ">=1.1.13-1 <1.2.0-0", + "xtend": ">=4.0.0 <4.1.0-0" + } + } + } + }, + "through2": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", + "dev": true, + "requires": { + "readable-stream": ">=1.0.33-1 <1.1.0-0", + "xtend": ">=4.0.0 <4.1.0-0" + }, + "dependencies": { + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + } + } + }, + "url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", + "dev": true, + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + } + } + } + } + }, + "watchpack": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz", + "integrity": "sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA==", + "dev": true, + "requires": { + "chokidar": "^2.0.2", + "graceful-fs": "^4.1.2", + "neo-async": "^2.5.0" + } + }, + "webpack": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.12.1.tgz", + "integrity": "sha512-7LOKQ+fpPtSvPlP++2rkDRU/8o6pJt00ezGPCksmeIzliOhiz0us4erBmNCW3VeVwH7tLIhv80zFYdOmmqU9BQ==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.5.12", + "@webassemblyjs/helper-module-context": "1.5.12", + "@webassemblyjs/wasm-edit": "1.5.12", + "@webassemblyjs/wasm-opt": "1.5.12", + "@webassemblyjs/wasm-parser": "1.5.12", + "acorn": "^5.6.2", + "acorn-dynamic-import": "^3.0.0", + "ajv": "^6.1.0", + "ajv-keywords": "^3.1.0", + "chrome-trace-event": "^1.0.0", + "enhanced-resolve": "^4.0.0", + "eslint-scope": "^3.7.1", + "json-parse-better-errors": "^1.0.2", + "loader-runner": "^2.3.0", + "loader-utils": "^1.1.0", + "memory-fs": "~0.4.1", + "micromatch": "^3.1.8", + "mkdirp": "~0.5.0", + "neo-async": "^2.5.0", + "node-libs-browser": "^2.0.0", + "schema-utils": "^0.4.4", + "tapable": "^1.0.0", + "uglifyjs-webpack-plugin": "^1.2.4", + "watchpack": "^1.5.0", + "webpack-sources": "^1.0.1" + }, + "dependencies": { + "acorn": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", + "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==", + "dev": true + }, + "acorn-dynamic-import": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-3.0.0.tgz", + "integrity": "sha512-zVWV8Z8lislJoOKKqdNMOB+s6+XV5WERty8MnKBeFgwA+19XJjJHs2RP5dzM57FftIs+jQnRToLiWazKr6sSWg==", + "dev": true, + "requires": { + "acorn": "^5.0.0" + } + }, + "ajv": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", + "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + } + } + }, + "webpack-cli": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.6.tgz", + "integrity": "sha512-0vEa83M7kJtxK/jUhlpZ27WHIOndz5mghWL2O53kiDoA9DIxSKnfqB92LoqEn77cT4f3H2cZm1BMEat/6AZz3A==", + "dev": true, + "requires": { + "chalk": "2.4.2", + "cross-spawn": "6.0.5", + "enhanced-resolve": "4.1.0", + "findup-sync": "3.0.0", + "global-modules": "2.0.0", + "import-local": "2.0.0", + "interpret": "1.2.0", + "loader-utils": "1.2.3", + "supports-color": "6.1.0", + "v8-compile-cache": "2.0.3", + "yargs": "13.2.4" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "findup-sync": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", + "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", + "dev": true, + "requires": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "interpret": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz", + "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==", + "dev": true + }, + "invert-kv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", + "dev": true + }, + "lcid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", + "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "dev": true, + "requires": { + "invert-kv": "^2.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "os-locale": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", + "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", + "dev": true, + "requires": { + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" + } + }, + "p-limit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", + "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yargs": { + "version": "13.2.4", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.4.tgz", + "integrity": "sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "os-locale": "^3.1.0", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.0" + } + }, + "yargs-parser": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", + "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "webpack-sources": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.1.tgz", + "integrity": "sha512-XSz38193PTo/1csJabKaV4b53uRVotlMgqJXm3s3eje0Bu6gQTxYDqpD38CmQfDBA+gN+QqaGjasuC8I/7eW3Q==", + "dev": true, + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "whatwg-fetch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz", + "integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==" + }, + "whet.extend": { + "version": "0.9.9", + "resolved": "https://registry.npmjs.org/whet.extend/-/whet.extend-0.9.9.tgz", + "integrity": "sha1-+HfVv2SMl+WqVC+twW1qJZucEaE=", + "dev": true, + "optional": true + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", + "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", + "dev": true + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "widest-line": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz", + "integrity": "sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==", + "dev": true, + "requires": { + "string-width": "^2.1.1" + } + }, + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", + "dev": true + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" + }, + "worker-farm": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", + "integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==", + "dev": true, + "requires": { + "errno": "~0.1.7" + } + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "dev": true, + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + } + } + }, + "wrap-fn": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/wrap-fn/-/wrap-fn-0.1.5.tgz", + "integrity": "sha1-8htuQQFv9KfjFyDbxjoJAWvfmEU=", + "dev": true, + "optional": true, + "requires": { + "co": "3.1.0" + }, + "dependencies": { + "co": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/co/-/co-3.1.0.tgz", + "integrity": "sha1-TqVOpaCJOBUxheFSEMaNkJK8G3g=", + "dev": true, + "optional": true + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", + "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } + }, + "write-file-atomic": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", + "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, + "xdg-basedir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", + "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=", + "dev": true + }, + "xml2js": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.2.4.tgz", + "integrity": "sha1-mltXf6HmzfiSPV4TcvejGIQ25E0=", + "dev": true, + "requires": { + "sax": ">=0.4.2" + } + }, + "xmlbuilder": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-0.4.2.tgz", + "integrity": "sha1-F3bWXz/brUcKCNhgTN6xxOVA/4M=", + "dev": true + }, + "xmlcreate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-1.0.2.tgz", + "integrity": "sha1-+mv3YqYKQT+z3Y9LA8WyaSONMI8=", + "dev": true + }, + "xmldom": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz", + "integrity": "sha1-1QH5ezvbQDr4757MIFcxh6rawOk=", + "dev": true + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", + "dev": true + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + }, + "yargs-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz", + "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=", + "dev": true, + "requires": { + "camelcase": "^3.0.0" + }, + "dependencies": { + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", + "dev": true + } + } + }, + "yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", + "dev": true, + "optional": true, + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "zlib-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/zlib-browserify/-/zlib-browserify-0.0.1.tgz", + "integrity": "sha1-T6akXQDbwV8xikr6HZr8Aljhdsw=", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ea56867 --- /dev/null +++ b/package.json @@ -0,0 +1,173 @@ +{ + "name": "internal-carto.js", + "version": "4.2.0", + "description": "CARTO javascript library", + "repository": { + "type": "git", + "url": "git://github.com/CartoDB/carto.js.git" + }, + "author": { + "name": "CARTO", + "url": "https://carto.com/" + }, + "contributors": [ + "Javier Álvarez ", + "Javier Álvarez ", + "Javier Arce ", + "Javier Santana ", + "Raul Ochoa ", + "Carlos Matallín ", + "Jaime Chapinal ", + "Nicklas Gummesson ", + "Francisco Dans ", + "Emilio García ", + "Ivan Malagon ", + "Ruben Moya ", + "Jesus Arroyo Torrens ", + "Iago Lastra ", + "Elena Torró ", + "Jesús Botella ", + "Alejandra Arri " + ], + "private": true, + "license": "BSD-3-Clause", + "dependencies": { + "backbone": "1.2.3", + "backbone-poller": "^1.1.3", + "camshaft-reference": "0.34.0", + "carto": "cartodb/carto#master", + "@carto/zera": "1.0.7", + "clip-path-polygon": "0.1.12", + "d3-array": "1.2.1", + "d3-format": "1.2.0", + "d3-time-format": "2.1.0", + "jquery": "2.1.4", + "mustache": "1.1.0", + "perfect-scrollbar": "git://github.com/CartoDB/perfect-scrollbar.git#master", + "postcss": "5.0.19", + "promise-polyfill": "^6.1.0", + "torque.js": "CartoDB/torque#master", + "underscore": "1.8.3", + "whatwg-fetch": "^2.0.3" + }, + "devDependencies": { + "babel-core": "^6.26.3", + "babel-loader": "^7.1.4", + "babel-plugin-transform-object-rest-spread": "^6.26.0", + "babel-preset-env": "^1.7.0", + "babel-preset-es2015": "~6.24.1", + "babelify": "^7.3.0", + "browserify": "13.0.0", + "browserify-shim": "3.8.12", + "cartoassets": "CartoDB/CartoAssets#master", + "eslint": "~4.18.2", + "eslint-config-semistandard": "~11.0.0", + "eslint-config-standard": "~10.2.1", + "eslint-plugin-import": "~2.7.0", + "eslint-plugin-node": "~5.2.0", + "eslint-plugin-promise": "~3.5.0", + "eslint-plugin-standard": "~3.0.1", + "findup-sync": "0.1.3", + "grunt": "0.4.5", + "grunt-aws": "^0.4.0", + "grunt-banner": "^0.6.0", + "grunt-browserify": "5.0.0", + "grunt-contrib-clean": "~0.5.0", + "grunt-contrib-concat": "~0.3.0", + "grunt-contrib-connect": "~0.11.2", + "grunt-contrib-copy": "~0.7.0", + "grunt-contrib-cssmin": "~0.7.0", + "grunt-contrib-imagemin": "~1.0.0", + "grunt-contrib-jasmine": "1.1.0", + "grunt-contrib-watch": "git://github.com/gruntjs/grunt-contrib-watch.git#b884948805940c663b1cbb91a3c28ba8afdebf78", + "grunt-eslint": "~20.1.0", + "grunt-exorcise": "2.1.0", + "grunt-fastly": "~0.1.3", + "grunt-gitinfo": "~0.1.7", + "grunt-prompt": "~1.3.0", + "grunt-replace": "0.6.2", + "grunt-sass": "2.0.0", + "grunt-terser": "^1.0.0", + "gulp": "3.8.10", + "gulp-iconfont": "1.0.0", + "gulp-iconfont-css": "0.0.9", + "gulp-install": "0.2.0", + "gulp-sketch": "0.0.7", + "jasmine-ajax": "git://github.com/nobuti/jasmine-ajax.git#master", + "jsdoc": "~3.5.5", + "jstify": "0.12.0", + "leaflet": "1.3.1", + "load-grunt-tasks": "~0.6.0", + "npm-watch": "^0.3.0", + "semver": "~5.4.0", + "source-map-support": "CartoDB/node-source-map-support#0.4.6-cdb1", + "time-grunt": "~0.3.1", + "uglifyjs-webpack-plugin": "^1.1.2", + "watchify": "3.4.0", + "webpack": "4.12.1", + "webpack-cli": "^3.0.4" + }, + "browserify": { + "transform": [ + "browserify-shim", + "jstify" + ] + }, + "browser": { + "cdb": "./src/cdb.js", + "cdb.config": "./src/cdb.config.js", + "cdb.core.util": "./src/core/util.js", + "cdb.core.Profiler": "./src/core/profiler.js", + "cdb.log": "./src/cdb.log.js", + "cdb.errors": "./src/cdb.errors.js", + "cdb.templates": "./src/cdb.templates.js", + "geojson": "./vendor/GeoJSON.js", + "html-css-sanitizer": "./vendor/html-css-sanitizer-bundle.js", + "mousewheel": "./vendor/mousewheel.js", + "mwheelIntent": "./vendor/mwheelIntent.js" + }, + "browserify-shim": { + "geojson": "GeoJSON", + "html-css-sanitizer": "html", + "mousewheel": { + "depends": [ + "jquery:jQuery" + ] + }, + "mwheelIntent": { + "depends": [ + "jquery:jQuery" + ] + } + }, + "files": [ + "dist", + "node_modules/cdb", + "src", + "themes", + "vendor" + ], + "main": "src/index.js", + "config": { + "root": "." + }, + "scripts": { + "test": "grunt test", + "test:browser": "grunt dev", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "build": "rm -rf dist/public; NODE_ENV=production webpack --progress --config webpack/webpack.config.js && NODE_ENV=production webpack --progress --config webpack/webpack.min.config.js", + "build:watch": "NODE_ENV=development webpack --progress -w --config webpack/webpack.config.js", + "build:internal": "grunt build", + "docs": "rm -rf docs/public; jsdoc --configure config/jsdoc/public-conf.json", + "docs:internal": "rm -rf docs/internal; jsdoc --configure config/jsdoc/internal-conf.json", + "bump": "npm version prerelease", + "bump:patch": "npm version patch", + "bump:minor": "npm version minor", + "postversion": "git push origin HEAD --follow-tags", + "release": "./scripts/release.sh" + }, + "watch": { + "docs": "src/**/*.js" + } +} diff --git a/scripts/deploy-docs-examples.sh b/scripts/deploy-docs-examples.sh new file mode 100644 index 0000000..146b911 --- /dev/null +++ b/scripts/deploy-docs-examples.sh @@ -0,0 +1,82 @@ +#!/bin/bash + +SEMVER_PATTERN="^v(0|[1-9]+)\.(0|[1-9]+)\.(0|[1-9]+)(-[a-z]+(\.[0-9a-z]+)?)?$" + +DOCS_DIR="docs" +EXAMPLES_DIR="examples" +TMP_DOCS_DIR="tmp_docs" +TMP_EXAMPLES_DIR="tmp_examples" + +CURRENT_COMMIT=`git rev-parse HEAD` +ORIGIN_URL=`git config --get remote.origin.url` +ORIGIN_URL_WITH_CREDENTIALS="https://$GITHUB_TOKEN@github.com/$TRAVIS_REPO_SLUG" + +# - Deploy only semver tags + +if [[ ! $TRAVIS_BRANCH =~ $SEMVER_PATTERN ]]; then + echo "Skip deploy: $TRAVIS_BRANCH" + exit 0 +fi + +# - Generation + +echo "Generating docs" +npm run docs +npm run docs:internal + +echo "Moving docs to tmp file" +mv $DOCS_DIR $TMP_DOCS_DIR + +echo "Moving examples to tmp file" +mv $EXAMPLES_DIR $TMP_EXAMPLES_DIR + +echo "Copying index.html" +cp config/jsdoc/index.html $TMP_DOCS_DIR/index.html || exit 1 + +# - Deployment + +echo "Starting docs/examples deployment" +echo "Target: gh-pages branch" + +echo "Fetching gh-pages branch" +git fetch origin gh-pages:refs/remotes/origin/gh-pages || exit 1 + +echo "Checking out gh-pages branch" +git checkout -- . || exit 1 +git checkout -b gh-pages origin/gh-pages || exit 1 + +echo "Copying source content to root" +rm -rf $DOCS_DIR/v4 || exit 1 +mv $TMP_DOCS_DIR/public $DOCS_DIR/v4 || exit 1 +rm -rf $DOCS_DIR/v4-internal || exit 1 +mv $TMP_DOCS_DIR/internal $DOCS_DIR/v4-internal || exit 1 +rm -rf $EXAMPLES_DIR/v4 || exit 1 +mkdir $EXAMPLES_DIR/v4 || exit 1 +mv $TMP_EXAMPLES_DIR/public $EXAMPLES_DIR/v4/public || exit 1 +mv $TMP_EXAMPLES_DIR/examples.json $EXAMPLES_DIR/v4/examples.json || exit 1 +mv $TMP_EXAMPLES_DIR/index.html $EXAMPLES_DIR/v4/index.html || exit 1 +mv $TMP_DOCS_DIR/index.html index.html || exit 1 + +echo "Adding version in index.html" +sed -i "s|%VERSION|$TRAVIS_BRANCH|g" index.html + +echo "Using unpkg CDN carto.js in the v4 examples" +CDN="https://unpkg.com/@carto/carto.js/carto.min.js" +OLD="../../../dist/public/carto.js" +sed -i "s|$OLD|$CDN|g" $EXAMPLES_DIR/v4/public/**/*.html +OLD="../dist/public/carto.js" +sed -i "s|$OLD|$CDN|g" $EXAMPLES_DIR/v4/index.html + +echo "Pushing new content to $ORIGIN_URL" +git config user.name "Cartofante" || exit 1 +git config user.email "systems@cartodb.com" || exit 1 + +git add index.html || exit 1 +git add $DOCS_DIR || exit 1 +git add $EXAMPLES_DIR || exit 1 +git commit --allow-empty -m "Update docs/examples for $TRAVIS_BRANCH $CURRENT_COMMIT" || exit 1 +git push --force --quiet "$ORIGIN_URL_WITH_CREDENTIALS" gh-pages > /dev/null 2>&1 + +echo "Docs/examples deployed successfully." + +exit 0 diff --git a/scripts/generate-package-json.js b/scripts/generate-package-json.js new file mode 100644 index 0000000..8dadb47 --- /dev/null +++ b/scripts/generate-package-json.js @@ -0,0 +1,14 @@ +/** + * This script adds the package.json with the updated version + * for the npm publish to the dist/public directory + */ +var fs = require('fs'); +var path = require('path'); +var version = require(path.join(__dirname, '../package.json')).version; +var dependencies = require(path.join(__dirname, '../package.json')).dependencies; +var placeholder = require(path.join(__dirname, './package.public.json')); + +placeholder.version = version; +placeholder.dependencies = dependencies; + +fs.writeFileSync(path.join(__dirname, '../dist/public/package.json'), JSON.stringify(placeholder, null, 2)); diff --git a/scripts/package.public.json b/scripts/package.public.json new file mode 100644 index 0000000..1e36fd9 --- /dev/null +++ b/scripts/package.public.json @@ -0,0 +1,16 @@ +{ + "name": "@carto/carto.js", + "version": "", + "description": "CARTO javascript library", + "main": "carto.js", + "repository": { + "type": "git", + "url": "git://github.com/CartoDB/carto.js.git" + }, + "dependencies": {}, + "author": { + "name": "CARTO", + "url": "https://carto.com/" + }, + "license": "BSD-3-Clause" +} diff --git a/scripts/release.sh b/scripts/release.sh new file mode 100755 index 0000000..97ae030 --- /dev/null +++ b/scripts/release.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +npm update + +VERSION=$(node --eval "console.log(require('./package.json').version);") + +npm test || exit 1 + +echo "Ready to publish CARTO.js version $VERSION" +echo "Has the version number been bumped?" +read -n1 -r -p "Press Ctrl+C to cancel, or any other key to continue." key + +npm run build + +echo "Uploading to CDN..." + +grunt publish_s3 +grunt invalidate + +echo "Uploading to npm..." + +node scripts/generate-package-json.js +cp README.md dist/public +cp CHANGELOG.md dist/public +cp LICENSE dist/public + +cd dist/public + +npm publish + +echo "All done." +echo "CDN at https://cartodb-libs.global.ssl.fastly.net and https://libs.cartocdn.com/" diff --git a/secrets.example.json b/secrets.example.json new file mode 100644 index 0000000..4aca9ed --- /dev/null +++ b/secrets.example.json @@ -0,0 +1,7 @@ +{ + "AWS_USER_S3_KEY": "", + "AWS_USER_S3_SECRET": "", + "AWS_S3_BUCKET": "", + "FASTLY_API_KEY": "", + "FASTLY_CARTODB_SERVICE": "" +} diff --git a/security.txt b/security.txt new file mode 100644 index 0000000..4751024 --- /dev/null +++ b/security.txt @@ -0,0 +1,4 @@ +Contact: mailto:security-issues@carto.com +Preferred-Languages: en +Policy: https://github.com/CartoDB/carto.js/blob/master/SECURITY-POLICY.md +Hiring: https://carto.com/careers diff --git a/src/analysis/analysis-model.js b/src/analysis/analysis-model.js new file mode 100644 index 0000000..1562928 --- /dev/null +++ b/src/analysis/analysis-model.js @@ -0,0 +1,229 @@ +var _ = require('underscore'); +var Backbone = require('backbone'); +var Model = require('../core/model'); +var util = require('../core/util'); + +var REQUIRED_OPTS = [ + 'camshaftReference', + 'engine' +]; + +var STATUS = { + PENDING: 'pending', + WAITING: 'waiting', + RUNNING: 'running', + FAILED: 'failed', + READY: 'ready' +}; + +var AnalysisModel = Model.extend({ + + initialize: function (attrs, opts) { + opts = opts || {}; + util.checkRequiredOpts(opts, REQUIRED_OPTS, 'AnalysisModel'); + + this._camshaftReference = opts.camshaftReference; + this._engine = opts.engine; + + this._initBinds(); + + // A hash that tracks which models (layers / dataviews) have + // this analysis model as their "source" + this._referencedBy = {}; + }, + + url: function () { + var url = this.get('url'); + if (url) { + if (this.get('apiKey')) { + url += '?api_key=' + this.get('apiKey'); + } else if (this.get('authToken')) { + var authToken = this.get('authToken'); + if (authToken instanceof Array) { + var tokens = _.map(authToken, function (token) { + return 'auth_token[]=' + token; + }); + url += '?' + tokens.join('&'); + } else { + url += '?auth_token=' + authToken; + } + } + return url; + } + }, + + setOk: function () { + this.unset('error'); + }, + + setError: function (error) { + this.set({ + error: error, + status: STATUS.FAILED + }); + }, + + _initBinds: function () { + this.bind('change:type', function () { + this.unbind(null, null, this); + this._initBinds(); + this._reload(); + }, this); + + _.each(this.getParamNames(), function (paramName) { + this.bind('change:' + paramName, this._reload, this); + }, this); + + this.bind('change:status', function () { + // If the status changed from any other status to "ready" + // and this analysis is the "source" of any layer or dataview, + // vis has to be reloaded. + if (this._hadStatus() && this.isReady() && this.isSourceOfAnyModel()) { + this._reload(); + } + }, this); + }, + + _hadStatus: function () { + return this.previous('status'); + }, + + _reload: function () { + this._engine.reload({ + error: this._onMapReloadError.bind(this) + }); + }, + + _onMapReloadError: function () { + this.set('status', STATUS.FAILED); + }, + + remove: function () { + this.trigger('destroy', this); + this.stopListening(); + }, + + findAnalysisById: function (analysisId) { + if (this.get('id') === analysisId) { + return this; + } + var sources = _.chain(this._getSourceNames()) + .map(function (sourceName) { + var source = this.get(sourceName); + if (source) { + return source.findAnalysisById(analysisId); + } + }, this) + .compact() + .value(); + + return sources[0]; + }, + + _getSourceNames: function () { + return this._camshaftReference.getSourceNamesForAnalysisType(this.get('type')); + }, + + isDone: function () { + return this._hasStatus([ STATUS.READY, STATUS.FAILED ]); + }, + + isLoading: function () { + return this._hasStatus([ STATUS.PENDING, STATUS.WAITING, STATUS.RUNNING ]); + }, + + isReady: function () { + return this._hasStatus(STATUS.READY); + }, + + isFailed: function () { + return this._hasStatus(STATUS.FAILED); + }, + + _hasStatus: function (statuses) { + if (!_.isArray(statuses)) { + statuses = [ statuses ]; + } + return _.contains(statuses, this._getStatus()); + }, + + _getStatus: function () { + return this.get('status'); + }, + + toJSON: function () { + var json = _.pick(this.attributes, 'id', 'type'); + json.params = _.pick(this.attributes, this.getParamNames()); + var sourceNames = this._getSourceNames(); + _.each(sourceNames, function (sourceName) { + var source = {}; + var sourceInfo = this.get(sourceName); + if (sourceInfo) { + source[sourceName] = sourceInfo.toJSON(); + _.extend(json.params, source); + } + }, this); + + return json; + }, + + getParamNames: function () { + return this._camshaftReference.getParamNamesForAnalysisType(this.get('type')); + }, + + /** + * Return an Array with the complete node list for this analysis. + */ + getNodes: function () { + // Add current node to the list + var nodes = [this]; + // Recursively iterate through the inputs ( source nodes have no inputs ) + if (this.get('type') !== 'source') { + _.forEach(this._getSourceNames(), function (sourceName) { + var source = this.get(sourceName); + if (source) { + nodes = nodes.concat(source.getNodes()); + } + }, this); + } + return nodes; + }, + + /** + * Return a Collection with the complete node list for this analysis. + */ + getNodesCollection: function () { + return new Backbone.Collection(this.getNodes()); + }, + + /** + * Compare two analysisModels. + */ + equals: function (analysisModel) { + if (!(analysisModel instanceof AnalysisModel)) { + return false; + } + // Since all analysis are created using the analysisFactory different ids ensure different nodes. + return this.get('id') === analysisModel.get('id'); + }, + + markAsSourceOf: function (model) { + this._referencedBy[model.cid] = true; + }, + + isSourceOfAnyModel: function () { + return Object.keys(this._referencedBy).length > 0; + }, + + isSourceOf: function (model) { + return !!this._referencedBy[model.cid]; + }, + + unmarkAsSourceOf: function (model) { + delete this._referencedBy[model.cid]; + } +}, { + STATUS: STATUS +}); + +module.exports = AnalysisModel; diff --git a/src/analysis/analysis-poller.js b/src/analysis/analysis-poller.js new file mode 100644 index 0000000..a9abe95 --- /dev/null +++ b/src/analysis/analysis-poller.js @@ -0,0 +1,60 @@ +var _ = require('underscore'); +var BackbonePoller = require('backbone-poller'); + +function AnalysisPoller () { + this._pollers = []; +} + +AnalysisPoller.CONFIG = { + START_DELAY: 1000, + MAX_DELAY: Infinity, + DELAY_MULTIPLIER: 1.5 +}; + +AnalysisPoller.prototype.resetAnalysisNodes = function (analysisModels) { + this.reset(); + _.each(analysisModels, function (analysisModel) { + this._poll(analysisModel); + }, this); +}; + +AnalysisPoller.prototype._poll = function (analysisModel) { + if (this._canBePolled(analysisModel)) { + var poller = this._createPoller(analysisModel); + poller.start(); + } +}; + +AnalysisPoller.prototype._canBePolled = function (analysisModel) { + return analysisModel.url() && !analysisModel.isDone(); +}; + +AnalysisPoller.prototype._findPoller = function (analysisModel) { + return _.find(this._pollers, function (poller) { + return poller.model === analysisModel; + }); +}; + +AnalysisPoller.prototype._createPoller = function (analysisModel) { + var pollerOptions = { + delay: [ + AnalysisPoller.CONFIG.START_DELAY, + AnalysisPoller.CONFIG.MAX_DELAY, + AnalysisPoller.CONFIG.DELAY_MULTIPLIER + ], + condition: function (analysisModel) { + return !analysisModel.isDone(); + } + }; + + var poller = BackbonePoller.get(analysisModel, pollerOptions); + this._pollers.push(poller); + return poller; +}; + +AnalysisPoller.prototype.reset = function () { + BackbonePoller.reset(); + this._pollers = []; +}; + +module.exports = AnalysisPoller; diff --git a/src/analysis/analysis-service.js b/src/analysis/analysis-service.js new file mode 100644 index 0000000..36175c2 --- /dev/null +++ b/src/analysis/analysis-service.js @@ -0,0 +1,155 @@ +var _ = require('underscore'); +var Backbone = require('backbone'); +var Analysis = require('./analysis-model'); +var camshaftReference = require('./camshaft-reference'); +var LayerTypes = require('../geo/map/layer-types.js'); + +var AnalysisService = function (opts) { + opts = opts || {}; + if (!opts.engine) { + throw new Error('engine is required'); + } + + this._engine = opts.engine; + this._apiKey = opts.apiKey; + this._authToken = opts.authToken; + this._camshaftReference = opts.camshaftReference || camshaftReference; // For testing purposes + + this._analysisNodes = new Backbone.Collection(); +}; + +/** + * Recursively generates a graph of analyses and returns the "root" node. + * For each node definition in the analysisDefinition: + * - If a node had been already created this method updates the attributes of the existing node. + * - Otherwise create a new node and index it by id into the `_analysisNodes` object. + */ +AnalysisService.prototype.analyse = function (analysisDefinition) { + analysisDefinition = _.clone(analysisDefinition); + var analysis = this.findNodeById(analysisDefinition.id); + var analysisAttrs = this._getAnalysisAttributesFromAnalysisDefinition(analysisDefinition); + + if (analysis) { + analysis.set(analysisAttrs); + } else { + if (this._apiKey) { + analysisAttrs.apiKey = this._apiKey; + } + if (this._authToken) { + analysisAttrs.authToken = this._authToken; + } + analysis = new Analysis(analysisAttrs, { + camshaftReference: this._camshaftReference, + engine: this._engine + }); + + this._analysisNodes.add(analysis); + analysis.bind('destroy', this._onAnalysisRemoved, this); + } + + return analysis; +}; + +/** + * This function is used to iterate over the analysis graph. + * It uses the camshaft reference to extract those parameters which are analysis nodes. And call analyse on them. + * + * This function wont be needed if we split the analysis definition in `params` and `inputs`. Where all analysis + * are garanted to be in the inputs object. + */ +AnalysisService.prototype._getAnalysisAttributesFromAnalysisDefinition = function (analysisDefinition) { + var analysisNodes = {}; + var analysisType = analysisDefinition.type; + var sourceNamesForAnalysisType = this._camshaftReference.getSourceNamesForAnalysisType(analysisType); + _.each(sourceNamesForAnalysisType, function (sourceName) { + var sourceParams = analysisDefinition.params[sourceName]; + if (sourceParams) { + analysisNodes[sourceName] = this.analyse(sourceParams); + } + }, this); + + return _.omit(_.extend(analysisDefinition, analysisDefinition.params, analysisNodes), 'params'); +}; + +/** + * Create a source analysis + * This function is used because some legacy viz.json files contains layers without `source` and have a `query` field instead. + * This query is translated into a analysis of type `source`. + */ +AnalysisService.prototype.createAnalysisForLayer = function (layerId, layerQuery) { + return this.analyse({ + id: layerId, + type: 'source', + params: { + query: layerQuery + } + }); +}; + +AnalysisService.prototype.findNodeById = function (id) { + return this._analysisNodes.get(id); +}; + +AnalysisService.prototype._onAnalysisRemoved = function (analysis) { + this._analysisNodes.remove(analysis); + analysis.unbind('destroy', this._onAnalysisRemoved); +}; + +/** + * Return all the analysis nodes without duplicates. + * The analyses are obtained from the layers and dataviews collections. + * @example + * We have the following analyses: (a0->a1->a2), (b0->a2) + * This method will give us: (a0->a1->a2), (a1->a2), (a2), (b0->a2) + */ +AnalysisService.getUniqueAnalysisNodes = function (layersCollection, dataviewsCollection) { + var uniqueAnalysisNodes = {}; + var analysisList = AnalysisService.getAnalysisList(layersCollection, dataviewsCollection); + _.each(analysisList, function (analysis) { + analysis.getNodesCollection().each(function (analysisNode) { + uniqueAnalysisNodes[analysisNode.get('id')] = analysisNode; + }); + }); + + return _.map(uniqueAnalysisNodes, function (analisis) { return analisis; }, this); +}; + +/** + * Return a list with all the analyses contained in the given collections. + * @example + * We have the following analyses: (a0->a1->a2), (b0->a2) + * This method will give us: (a0->a1->a2), (b0->a2) + */ +AnalysisService.getAnalysisList = function (layersCollection, dataviewsCollection) { + var layerAnalyses = _getAnalysesFromLayers(layersCollection); + var dataviewsAnalyses = _getAnalysesFromDataviews(dataviewsCollection); + return layerAnalyses.concat(dataviewsAnalyses); +}; + +function _getAnalysesFromLayers (layersCollection) { + var layers = _getCartoDBAndTorqueLayers(layersCollection); + return _.chain(layers) + .map(function (layer) { + return layer.getSource(); + }) + .compact() + .value(); +} + +function _getAnalysesFromDataviews (dataviewsCollection) { + return dataviewsCollection.chain() + .map(function (dataview) { + return dataview.getSource(); + }) + .compact() + .value(); +} + +function _getCartoDBAndTorqueLayers (layersCollection) { + return layersCollection.filter(function (layer) { + // Carto and torque layers are supposed to have a source + return LayerTypes.isCartoDBLayer(layer) || LayerTypes.isTorqueLayer(layer); + }); +} + +module.exports = AnalysisService; diff --git a/src/analysis/camshaft-reference.js b/src/analysis/camshaft-reference.js new file mode 100644 index 0000000..9653d8a --- /dev/null +++ b/src/analysis/camshaft-reference.js @@ -0,0 +1,51 @@ +var camshaftReference = require('camshaft-reference').getVersion('latest'); +var PARAM_TYPES = { + NODE: 'node', + NUMBER: 'number', + STRING: 'string', + ENUM: 'enum' +}; + +var SOURCE_ANALYSIS_TYPE = 'source'; +var ANALYSIS_TYPE_TO_SOURCE_PARAM_NAMES_MAP = {}; +ANALYSIS_TYPE_TO_SOURCE_PARAM_NAMES_MAP[SOURCE_ANALYSIS_TYPE] = []; +var ANALYSIS_TYPE_TO_PARAM_NAMES_MAP = {}; + +var analysesReference = camshaftReference.analyses; +if (!analysesReference) { + throw new Error('Error loading the reference for Camshaft analyses'); +} + +// Populate the analysis source and param names maps. +for (var analysisType in analysesReference) { + var analysisParams = analysesReference[analysisType].params; + for (var paramName in analysisParams) { + ANALYSIS_TYPE_TO_PARAM_NAMES_MAP[analysisType] = ANALYSIS_TYPE_TO_PARAM_NAMES_MAP[analysisType] || []; + ANALYSIS_TYPE_TO_PARAM_NAMES_MAP[analysisType].push(paramName); + ANALYSIS_TYPE_TO_SOURCE_PARAM_NAMES_MAP[analysisType] = ANALYSIS_TYPE_TO_SOURCE_PARAM_NAMES_MAP[analysisType] || []; + var paramType = analysisParams[paramName].type; + if (paramType === PARAM_TYPES.NODE) { + ANALYSIS_TYPE_TO_SOURCE_PARAM_NAMES_MAP[analysisType].push(paramName); + } + } +} + +module.exports = { + getSourceNamesForAnalysisType: function (analysisType) { + var sourceNames = ANALYSIS_TYPE_TO_SOURCE_PARAM_NAMES_MAP[analysisType]; + if (!sourceNames) { + throw new Error('source names for analysis of type ' + analysisType + " couldn't be found"); + } + + return sourceNames; + }, + + getParamNamesForAnalysisType: function (analysisType) { + var paramNames = ANALYSIS_TYPE_TO_PARAM_NAMES_MAP[analysisType]; + if (!paramNames) { + throw new Error('param names for analysis of type ' + analysisType + " couldn't be found"); + } + + return paramNames; + } +}; diff --git a/src/api/create-vis.js b/src/api/create-vis.js new file mode 100644 index 0000000..621185b --- /dev/null +++ b/src/api/create-vis.js @@ -0,0 +1,171 @@ +var _ = require('underscore'); +var VisView = require('../vis/vis-view'); +var VisModel = require('../vis/vis'); +var Loader = require('../core/loader'); +var VizJSON = require('./vizjson'); + +var DEFAULT_OPTIONS = { + tiles_loader: true, + loaderControl: true, + infowindow: true, // TODO: it seems that this is no longer used + tooltip: true, // TODO: it seems that this is no longer used + logo: true, + show_empty_infowindow_fields: false, + showLimitErrors: false, + interactiveFeatures: false +}; + +var createVis = function (el, vizjson, options) { + if (typeof el === 'string') { + el = document.getElementById(el); + } + if (!el) { + throw new TypeError('a valid DOM element or selector must be provided'); + } + if (!vizjson) { + throw new TypeError('a vizjson URL or object must be provided'); + } + + var isProtocolHTTPs = window && window.location.protocol && window.location.protocol === 'https:'; + options = _.defaults(options || {}, DEFAULT_OPTIONS); + + var visModel = new VisModel({ + apiKey: options.apiKey, + authToken: options.authToken, + showEmptyInfowindowFields: options.show_empty_infowindow_fields === true, + showLimitErrors: options.showLimitErrors === true, + https: isProtocolHTTPs || options.https === true, + interactiveFeatures: options.interactiveFeatures + }); + + new VisView({ // eslint-disable-line + el: el, + model: visModel, + settingsModel: visModel.settings + }); + + if (typeof vizjson === 'string') { + var url = vizjson; + Loader.get(url, function (vizjson) { + if (vizjson) { + loadVizJSON(el, visModel, vizjson, options); + } else { + throw new Error('error fetching viz.json file'); + } + }); + } else { + loadVizJSON(el, visModel, vizjson, options); + } + + return visModel; +}; + +var loadVizJSON = function (el, visModel, vizjsonData, options) { + var vizjson = new VizJSON(vizjsonData); + applyOptionsToVizJSON(vizjson, options); + + var showLegends = true; + if (_.isBoolean(options.legends)) { + showLegends = options.legends; + } else if (vizjson.options && _.isBoolean(vizjson.options.legends)) { + showLegends = vizjson.options.legends; + } + + var showLayerSelector = true; + if (_.isBoolean(options.layer_selector)) { + showLayerSelector = options.layer_selector; + } else if (vizjson.options && _.isBoolean(vizjson.options.layer_selector)) { + showLayerSelector = vizjson.options.layer_selector; + } + + var layerSelectorEnabled = true; + if (_.isBoolean(options.layerSelectorEnabled)) { + layerSelectorEnabled = options.layerSelectorEnabled; + } + + visModel.set({ + title: vizjson.title, + description: vizjson.description, + https: visModel.get('https') || vizjson.https === true + }); + + visModel.setSettings({ + showLegends: showLegends, + showLayerSelector: showLayerSelector, + layerSelectorEnabled: layerSelectorEnabled + }); + + visModel.load(vizjson); + + if (!options.skipMapInstantiation) { + visModel.instantiateMap(); + } +}; + +var applyOptionsToVizJSON = function (vizjson, options) { + vizjson.options = vizjson.options || {}; + vizjson.options.scrollwheel = _.isBoolean(options.scrollwheel) ? options.scrollwheel : vizjson.options.scrollwheel; + + if (!options.tiles_loader || !options.loaderControl) { + vizjson.removeLoaderOverlay(); + } + + if (options.searchControl === true) { + vizjson.addSearchOverlay(); + } else if (options.searchControl === false) { + vizjson.removeSearchOverlay(); + } + + if ((options.title && vizjson.title) || (options.description && vizjson.description)) { + vizjson.addHeaderOverlay(options.title, options.description, options.shareable); + } + + if (options.zoomControl !== undefined && !options.zoomControl) { + vizjson.removeZoomOverlay(); + } + + if (options.logo === false) { + vizjson.removeLogoOverlay(); + } + + if (_.has(options, 'vector')) { + vizjson.setVector(options.vector); + } + + // if bounds are present zoom and center will not taken into account + var zoom = parseInt(options.zoom, 10); + if (!isNaN(zoom)) { + vizjson.setZoom(zoom); + } + + // Center coordinates? + var centerLat = parseFloat(options.center_lat); + var centerLon = parseFloat(options.center_lon); + if (!isNaN(centerLat) && !isNaN(centerLon)) { + vizjson.setCenter([centerLat, centerLon]); + } + + // Center object + if (options.center !== undefined) { + vizjson.setCenter(options.center); + } + + // Bounds? + var swLat = parseFloat(options.sw_lat); + var swLon = parseFloat(options.sw_lon); + var neLat = parseFloat(options.ne_lat); + var neLon = parseFloat(options.ne_lon); + + if (!isNaN(swLat) && !isNaN(swLon) && !isNaN(neLat) && !isNaN(neLon)) { + vizjson.setBounds([ + [ swLat, swLon ], + [ neLat, neLon ] + ]); + } + + if (options.gmaps_base_type) { + vizjson.enforceGMapsBaseLayer(options.gmaps_base_type, options.gmaps_style); + } +}; + +module.exports = createVis; diff --git a/src/api/promise.js b/src/api/promise.js new file mode 100644 index 0000000..a658749 --- /dev/null +++ b/src/api/promise.js @@ -0,0 +1,17 @@ +var _ = require('underscore'); +var Backbone = require('backbone'); + +// NOTE only for usage in non-core bundles (where Backbone is available) +function Promise () { +} + +_.extend(Promise.prototype, Backbone.Events, { + done: function (fn) { + return this.bind('done', fn); + }, + error: function (fn) { + return this.bind('error', fn); + } +}); + +module.exports = Promise; diff --git a/src/api/sql.js b/src/api/sql.js new file mode 100644 index 0000000..c428208 --- /dev/null +++ b/src/api/sql.js @@ -0,0 +1,704 @@ +var _ = require('underscore'); +var $ = require('jquery'); +var Mustache = require('mustache'); +var Promise = require('./promise'); + +var NO_BOUNDS_ERROR_MESSAGE = 'No bounds'; + +// Variable that defines if a query should be using get method or post method +var MAX_LENGTH_GET_QUERY = 1024; + +function SQL (options) { + if (window.cdb === this || window === this) { + return new SQL(options); + } + if (!options.user) { + throw new Error('user should be provided'); + } + var loc = String(window.location.protocol); + loc = loc.slice(0, loc.length - 1); + if (loc === 'file') { + loc = 'https'; + } + + this.options = _.defaults(options, { + version: 'v2', + protocol: loc, + jsonp: !$.support.cors, + abortable: false + }); + + if (!this.options.sql_api_template) { + var opts = this.options; + var template = null; + if (opts && opts.completeDomain) { + template = opts.completeDomain; + } else { + var host = opts.host || 'carto.com'; + var protocol = opts.protocol || 'https'; + template = protocol + '://{user}.' + host; + } + this.options.sql_api_template = template; + } +} + +SQL.prototype._host = function () { + var opts = this.options; + return opts.sql_api_template.replace('{user}', opts.user) + '/api/' + opts.version + '/sql'; +}; + +/** + * var sql = new SQL('cartodb_username'); + * sql.execute("select * from {{ table }} where id = {{ id }}", { + * table: 'test', + * id: '1' + * }) + */ +SQL.prototype.execute = function (sql, vars, options, callback) { + var promise = new Promise(); + if (!sql) { + throw new TypeError('sql should not be null'); + } + // setup arguments + var args = arguments; + var fn = args[args.length - 1]; + if (_.isFunction(fn)) { + callback = fn; + } + options = _.defaults(options || {}, this.options); + var params = { + type: 'get', + dataType: 'json', + crossDomain: true + }; + + if (this._xhr && this.options.abortable === true) { + this._xhr.abort(); + } + + if (options.cache !== undefined) { + params.cache = options.cache; + } + + if (options.jsonp) { + delete params.crossDomain; + if (options.jsonpCallback) { + params.jsonpCallback = options.jsonpCallback; + } + params.dataType = 'jsonp'; + } + + // Substitute mapnik tokens + // resolution at zoom level 0 + var res = '156543.03515625'; + // full webmercator extent + var ext = 'ST_MakeEnvelope(-20037508.5,-20037508.5,20037508.5,20037508.5,3857)'; + sql = sql.replace('!bbox!', ext) + .replace('!pixel_width!', res) + .replace('!pixel_height!', res); + + // create query + var query = Mustache.render(sql, vars); + + // check method: if we are going to send by get or by post + var isGetRequest = query.length < MAX_LENGTH_GET_QUERY; + + // generate url depending on the http method + var reqParams = ['format', 'dp', 'api_key']; + // request params + if (options.extra_params) { + reqParams = reqParams.concat(options.extra_params); + } + + params.url = this._host(); + var i, r, v; + if (isGetRequest) { + var q = 'q=' + encodeURIComponent(query); + for (i in reqParams) { + r = reqParams[i]; + v = options[r]; + if (v != null) { + q += '&' + r + '=' + v; + } + } + + params.url += '?' + q; + } else { + var objPost = {'q': query}; + for (i in reqParams) { + r = reqParams[i]; + v = options[r]; + if (v != null) { + objPost[r] = v; + } + } + + params.data = objPost; + params.type = 'post'; + } + + // wrap success and error functions + var success = options.success; + var error = options.error; + if (success) delete options.success; + if (error) delete error.success; + + params.error = function (resp) { + var res = resp.responseText || resp.response; + var errors = res && JSON.parse(res); + promise.trigger('error', errors && errors.error, resp); + if (error) error(resp); + if (callback) callback(resp); + }; + params.success = function (resp, status, xhr) { + // manage rewest + if (status === undefined) { + status = resp.status; + xhr = resp; + resp = JSON.parse(resp.response); + } + // Timeout explanation. CartoDB.js ticket #336 + // From St.Ov.: "what setTimeout does is add a new event to the browser event queue + // and the rendering engine is already in that queue (not entirely true, but close enough) + // so it gets executed before the setTimeout event." + setTimeout(function () { + promise.trigger('done', resp, status, xhr); + if (success) success(resp, status, xhr); + if (callback) callback(null, resp); + }, 0); + }; + + params.complete = function () { + this._xhr = null; + }.bind(this); + + // call ajax + delete options.jsonp; + this._xhr = $.ajax(_.extend(params, options)); + + return promise; +}; + +SQL.prototype.getBounds = function (sql, vars, options, callback) { + var promise = new Promise(); + var args = arguments; + var fn = args[args.length - 1]; + if (_.isFunction(fn)) { + callback = fn; + } + var s = 'SELECT ST_XMin(ST_Extent(the_geom)) as minx,' + + ' ST_YMin(ST_Extent(the_geom)) as miny,' + + ' ST_XMax(ST_Extent(the_geom)) as maxx,' + + ' ST_YMax(ST_Extent(the_geom)) as maxy' + + ' from ({{{ sql }}}) as subq'; + sql = Mustache.render(sql, vars); + this.execute(s, { sql: sql }, options) + .done(function (result) { + if (result.rows && result.rows.length > 0 && result.rows[0].maxx != null) { + var c = result.rows[0]; + var minlat = -85.0511; + var maxlat = 85.0511; + var minlon = -179; + var maxlon = 179; + + var clamp = function (x, min, max) { + return x < min ? min : x > max ? max : x; + }; + + var lon0 = clamp(c.maxx, minlon, maxlon); + var lon1 = clamp(c.minx, minlon, maxlon); + var lat0 = clamp(c.maxy, minlat, maxlat); + var lat1 = clamp(c.miny, minlat, maxlat); + + var bounds = [[lat0, lon0], [lat1, lon1]]; + promise.trigger('done', bounds); + callback && callback(null, bounds); + } else { + var err = [NO_BOUNDS_ERROR_MESSAGE]; + promise.trigger('error', err); + callback && callback(err); + } + }) + .error(function (err) { + promise.trigger('error', err); + callback && callback(err); + }); + + return promise; +}; + +/** + * var people_under_10 = sql + * .table('test') + * .columns(['age', 'column2']) + * .filter('age < 10') + * .limit(15) + * .order_by('age') + * + * people_under_10(function(results) { + * }) + */ + +SQL.prototype.table = function (name) { + var _name = name; + var _filters; + var _columns = []; + var _limit; + var _order; + var _orderDir; + var _sql = this; + + function _table () { + _table.fetch.apply(_table, arguments); + } + + _table.fetch = function (vars) { + vars = vars || {}; + var args = arguments; + var fn = args[args.length - 1]; + if (_.isFunction(fn) && args.length === 1) { + vars = {}; + } + _sql.execute(_table.sql(), vars, fn); + }; + + _table.sql = function () { + var s = 'select'; + if (_columns.length) { + s += ' ' + _columns.join(',') + ' '; + } else { + s += ' * '; + } + + s += 'from ' + _name; + + if (_filters) { + s += ' where ' + _filters; + } + if (_limit) { + s += ' limit ' + _limit; + } + if (_order) { + s += ' order by ' + _order; + } + if (_orderDir) { + s += ' ' + _orderDir; + } + + return s; + }; + + _table.filter = function (f) { + _filters = f; + return _table; + }; + + _table.order_by = function (o) { + _order = o; + return _table; + }; + _table.asc = function () { + _orderDir = 'asc'; + return _table; + }; + + _table.desc = function () { + _orderDir = 'desc'; + return _table; + }; + + _table.columns = function (c) { + _columns = c; + return _table; + }; + + _table.limit = function (l) { + _limit = l; + return _table; + }; + + return _table; +}; + +/* + * sql.filter(sql.f().distance('< 10km') + */ +/* SQL.geoFilter = function() { + var _sql; + function f() {} + + f.distance = function(qty) { + qty.replace('km', '*1000') + _sql += 'st_distance(the_geom) ' + qty + } + f.or = function() { + } + + f.and = function() { + } + return f; +} +*/ +function arrayAgg (s) { + return JSON.parse(s.replace(/^{/, '[').replace(/}$/, ']')); +} + +SQL.prototype.describeString = function (sql, column, callback) { + var s = [ + 'WITH t as (', + ' SELECT count(*) as total,', + ' count(DISTINCT {{column}}) as ndist', + ' FROM ({{sql}}) _wrap', + ' ), a as (', + ' SELECT ', + ' count(*) cnt, ', + ' {{column}}', + ' FROM ', + ' ({{sql}}) _wrap ', + ' GROUP BY ', + ' {{column}} ', + ' ORDER BY ', + ' cnt DESC', + ' ), b As (', + ' SELECT', + ' row_number() OVER (ORDER BY cnt DESC) rn,', + ' cnt', + ' FROM a', + ' ), c As (', + ' SELECT ', + ' sum(cnt) OVER (ORDER BY rn ASC) / t.total cumperc,', + ' rn,', + ' cnt ', + ' FROM b, t', + ' LIMIT 10', + ' ),', + 'stats as (', + 'select count(distinct({{column}})) as uniq, ', + ' count(*) as cnt, ', + ' sum(case when COALESCE(NULLIF({{column}},\'\')) is null then 1 else 0 end)::numeric as null_count, ', + ' sum(case when COALESCE(NULLIF({{column}},\'\')) is null then 1 else 0 end)::numeric / count(*)::numeric as null_ratio, ', + // ' CDB_DistinctMeasure(array_agg({{column}}::text)) as cat_weight ', + ' (SELECT max(cumperc) weight FROM c) As skew ', + 'from ({{sql}}) __wrap', + '),', + 'hist as (', + 'select array_agg(row(d, c)) array_agg from (select distinct({{column}}) d, count(*) as c from ({{sql}}) __wrap, stats group by 1 limit 100) _a', + ')', + 'select * from stats, hist' + ]; + + var query = Mustache.render(s.join('\n'), { + column: column, + sql: sql + }); + + var normalizeName = function (str) { + var normalizedStr = str.replace(/^"(.+(?="$))?"$/, '$1'); // removes surrounding quotes + return normalizedStr.replace(/""/g, '"'); // removes duplicated quotes + }; + + this.execute(query, function (err, data) { + if (err) { + callback(err); + return; + } + + var row = data.rows[0]; + var weight = 0; + var histogram = []; + + try { + var s = arrayAgg(row.array_agg); + + histogram = _(s).map(function (row) { + var r = row.match(/\((.*),(\d+)/); + var name = normalizeName(r[1]); + return [name, +r[2]]; + }); + + weight = row.skew * (1 - row.null_ratio) * (1 - row.uniq / row.cnt) * (row.uniq > 1 ? 1 : 0); + } catch (e) { + + } + + callback(null, { + type: 'string', + hist: histogram, + distinct: row.uniq, + count: row.cnt, + null_count: row.null_count, + null_ratio: row.null_ratio, + skew: row.skew, + weight: weight + }); + }); +}; + +SQL.prototype.describeDate = function (sql, column, callback) { + var s = [ + 'with minimum as (', + 'SELECT min({{column}}) as start_time FROM ({{sql}}) _wrap), ', + 'maximum as (SELECT max({{column}}) as end_time FROM ({{sql}}) _wrap), ', + 'null_ratio as (SELECT sum(case when {{column}} is null then 1 else 0 end)::numeric / count(*)::numeric as null_ratio FROM ({{sql}}) _wrap), ', + 'moments as (SELECT count(DISTINCT {{column}}) as moments FROM ({{sql}}) _wrap)', + 'SELECT * FROM minimum, maximum, moments, null_ratio' + ]; + var query = Mustache.render(s.join('\n'), { + column: column, + sql: sql + }); + + this.execute(query, function (err, data) { + if (err) { + callback(err); + return; + } + + var row = data.rows[0]; + var e = new Date(row.end_time); + var s = new Date(row.start_time); + + var steps = Math.min(row.moments, 1024); + + callback(null, { + type: 'date', + start_time: s, + end_time: e, + range: e - s, + steps: steps, + null_ratio: row.null_ratio + }); + }); +}; + +SQL.prototype.describeBoolean = function (sql, column, callback) { + var s = [ + 'with stats as (', + 'select count(distinct({{column}})) as uniq,', + 'count(*) as cnt', + 'from ({{sql}}) _wrap ', + '),', + 'null_ratio as (', + 'SELECT sum(case when {{column}} is null then 1 else 0 end)::numeric / count(*)::numeric as null_ratio FROM ({{sql}}) _wrap), ', + 'true_ratio as (', + 'SELECT sum(case when {{column}} is true then 1 else 0 end)::numeric / count(*)::numeric as true_ratio FROM ({{sql}}) _wrap) ', + 'SELECT * FROM true_ratio, null_ratio, stats' + ]; + var query = Mustache.render(s.join('\n'), { + column: column, + sql: sql + }); + + this.execute(query, function (err, data) { + if (err) { + callback(err); + return; + } + + var row = data.rows[0]; + + callback(null, { + type: 'boolean', + null_ratio: row.null_ratio, + true_ratio: row.true_ratio, + distinct: row.uniq, + count: row.cnt + }); + }); +}; + +SQL.prototype.describeGeom = function (sql, column, callback) { + var s = [ + 'with geotype as (', + 'select st_geometrytype({{column}}) as geometry_type from ({{sql}}) _w where {{column}} is not null limit 1', + ')', + 'select * from geotype' + ]; + + var query = Mustache.render(s.join('\n'), { + column: column, + sql: sql + }); + function simplifyType (g) { + return { + 'st_multipolygon': 'polygon', + 'st_polygon': 'polygon', + 'st_multilinestring': 'line', + 'st_linestring': 'line', + 'st_multipoint': 'point', + 'st_point': 'point' + }[g.toLowerCase()]; + } + + this.execute(query, function (err, data) { + if (err) { + callback(err); + return; + } + + var row = data.rows[0]; + callback(null, { + type: 'geom', + // lon,lat -> lat, lon + geometry_type: row.geometry_type, + simplified_geometry_type: simplifyType(row.geometry_type) + }); + }); +}; + +SQL.prototype.columns = function (sql, options, callback) { + var args = arguments; + var fn = args[args.length - 1]; + if (_.isFunction(fn)) { + callback = fn; + } + var s = 'select * from (' + sql + ') __wrap limit 0'; + var exclude = ['cartodb_id', 'latitude', 'longitude', 'created_at', 'updated_at', 'lat', 'lon', 'the_geom_webmercator']; + this.execute(s, function (err, data) { + if (err) { + callback(err); + return; + } + + var t = {}; + for (var i in data.fields) { + if (exclude.indexOf(i) === -1) { + t[i] = data.fields[i].type; + } + } + callback(null, t); + }); +}; + +SQL.prototype.describeFloat = function (sql, column, callback) { + var s = [ + 'with stats as (', + 'select min({{column}}) as min,', + 'max({{column}}) as max,', + 'avg({{column}}) as avg,', + 'count(DISTINCT {{column}}) as cnt,', + 'count(distinct({{column}})) as uniq,', + 'count(*) as cnt,', + 'sum(case when {{column}} is null then 1 else 0 end)::numeric / count(*)::numeric as null_ratio,', + 'stddev_pop({{column}}) / count({{column}}) as stddev,', + 'CASE WHEN abs(avg({{column}})) > 1e-7 THEN stddev({{column}}) / abs(avg({{column}})) ELSE 1e12 END as stddevmean,', + 'CDB_DistType(array_agg("{{column}}"::numeric)) as dist_type ', + 'from ({{sql}}) _wrap ', + '),', + 'params as (select min(a) as min, (max(a) - min(a)) / 7 as diff from ( select {{column}} as a from ({{sql}}) _table_sql where {{column}} is not null ) as foo ),', + 'histogram as (', + 'select array_agg(row(bucket, range, freq)) as hist from (', + 'select CASE WHEN uniq > 1 then width_bucket({{column}}, min-0.01*abs(min), max+0.01*abs(max), 100) ELSE 1 END as bucket,', + 'numrange(min({{column}})::numeric, max({{column}})::numeric) as range,', + 'count(*) as freq', + 'from ({{sql}}) _w, stats', + 'group by 1', + 'order by 1', + ') __wrap', + '),', + 'hist as (', + 'select array_agg(row(d, c)) cat_hist from (select distinct({{column}}) d, count(*) as c from ({{sql}}) __wrap, stats group by 1 limit 100) _a', + '),', + 'buckets as (', + 'select CDB_QuantileBins(array_agg(distinct({{column}}::numeric)), 7) as quantiles, ', + ' (select array_agg(x::numeric) FROM (SELECT (min + n * diff)::numeric as x FROM generate_series(1,7) n, params) p) as equalint,', + // ' CDB_EqualIntervalBins(array_agg({{column}}::numeric), 7) as equalint, ', + ' CDB_JenksBins(array_agg(distinct({{column}}::numeric)), 7) as jenks, ', + ' CDB_HeadsTailsBins(array_agg(distinct({{column}}::numeric)), 7) as headtails ', + 'from ({{sql}}) _table_sql where {{column}} is not null', + ')', + 'select * from histogram, stats, buckets, hist' + ]; + + var query = Mustache.render(s.join('\n'), { + column: column, + sql: sql + }); + + this.execute(query, function (err, data) { + if (err) { + callback(err); + return; + } + + var row = data.rows[0]; + var s = arrayAgg(row.hist); + var h = arrayAgg(row.cat_hist); + callback(null, { + type: 'number', + cat_hist: + _(h).map(function (row) { + var r = row.match(/\((.*),(\d+)/); + return [+r[1], +r[2]]; + }), + hist: _(s).map(function (row) { + if (row.indexOf('empty') > -1) return; + var els = row.split('"'); + return { index: els[0].replace(/\D/g, ''), + range: els[1].split(',').map(function (d) { return d.replace(/\D/g, ''); }), + freq: els[2].replace(/\D/g, '') }; + }), + stddev: row.stddev, + null_ratio: row.null_ratio, + count: row.cnt, + distinct: row.uniq, + // lstddev: row.lstddev, + avg: row.avg, + max: row.max, + min: row.min, + stddevmean: row.stddevmean, + weight: (row.uniq > 1 ? 1 : 0) * (1 - row.null_ratio) * (row.stddev < -1 ? 1 : (row.stddev < 1 ? 0.5 : (row.stddev < 3 ? 0.25 : 0.1))), + quantiles: row.quantiles, + equalint: row.equalint, + jenks: row.jenks, + headtails: row.headtails, + dist_type: row.dist_type + }); + }); +}; + +// describe a column +SQL.prototype.describe = function (sql, column, options) { + var self = this; + var args = arguments; + var fn = args[args.length - 1]; + if (_.isFunction(fn)) { + var _callback = fn; + } + var callback = function (err, data) { + if (err) { + _callback(err); + return; + } + + data.column = column; + _callback(null, data); + }; + var s = 'select * from (' + sql + ') __wrap limit 0'; + this.execute(s, function (err, data) { + if (err) { + callback(err); + return; + } + + var type = (options && options.type) ? options.type : data.fields[column].type; + + if (!type) { + callback(new Error('column does not exist')); + } else if (type === 'string') { + self.describeString(sql, column, callback); + } else if (type === 'number') { + self.describeFloat(sql, column, callback); + } else if (type === 'geometry') { + self.describeGeom(sql, column, callback); + } else if (type === 'date') { + self.describeDate(sql, column, callback); + } else if (type === 'boolean') { + self.describeBoolean(sql, column, callback); + } else { + callback(new Error('column type is not supported')); + } + }); +}; + +module.exports = SQL; diff --git a/src/api/v4/README.md b/src/api/v4/README.md new file mode 100644 index 0000000..b8835b5 --- /dev/null +++ b/src/api/v4/README.md @@ -0,0 +1,47 @@ +# V4 API + +This folder contains the source files used in the `v4 public api`. + +This api build using wrappers over the internal objects, those wrappers have an easy-to-use public methods, +the reference to the internal objects can be obtained with the `$getInternalModel` method. + +The `$` before a method name is a naming convention. Those methods shall not be exposed in the public API +but can be used from different files. (Public only for developers). + +## Api structure + +All the api methods and objects are exposed through the public `carto` object. + +- `carto.client` : The main object used in a CARTO.js app +- `carto.source`: Namespace for the sources. + - `Dataset`: Get all the data from a table + - `SQL`: Get the data from a custom SQL query +- `carto.style`: Namespace for the styles + - `CartoCSS`: Constructor to build layer styles. +- `carto.layer` : Namespace for the layers + - `Layer`: Constructor to build a Layer object +- `carto.dataview` : Namespace for the dataviews + - `Formula`: Constructor to build a Formula dataview + - `Category`: Constructor to build a Category dataview + - `Histogram`: Constructor to build a Histogram dataview + - `Time Series`: Constructor to build a Time Series dataview +- `carto.filter` : Namespace for the filters + - `BoundingBox`: Constructor to build a BoundingBox filter + - `BoundingBoxLeaflet`: Constructor to build a BoundingBoxLeaflet filter + +- `carto.operation` : Enum with the operations available. +- `carto.dataview.status` : Enum with the dataview statuses available. + +## Usage + +### Common.js + +```javascript +const carto = require('cartojs'); +``` + +### Loading CARTO.js from a CDN + +```javascript +window.carto; // All the api is available here +``` diff --git a/src/api/v4/client.js b/src/api/v4/client.js new file mode 100644 index 0000000..9eed547 --- /dev/null +++ b/src/api/v4/client.js @@ -0,0 +1,521 @@ +var _ = require('underscore'); +var Backbone = require('backbone'); +var CartoError = require('./error-handling/carto-error'); +var Engine = require('../../engine'); +var Events = require('./events'); +var LayerBase = require('./layer/base'); +var Layers = require('./layers'); +var VERSION = require('../../../package.json').version; +var CartoValidationError = require('./error-handling/carto-validation-error'); +var utils = require('../../core/util'); + +function getValidationError (code) { + return new CartoValidationError('client', code); +} + +const DEFAULT_SERVER_URL = 'https://{username}.carto.com'; + +/** + * This is the entry point for a CARTO.js application. + * + * A CARTO client allows managing layers and dataviews. Some operations like addding a layer or a dataview are asynchronous. + * The client takes care of the communication between CARTO.js and the server for you. + * + * To create a new client you need a CARTO account, where you will be able to get + * your API key and username. + * + * If you want to learn more about authorization and authentication, please read the authorization fundamentals section of our Developer Center. + * + * @param {object} settings + * @param {string} settings.apiKey - API key used to authenticate against CARTO + * @param {string} settings.username - Name of the user + * @param {string} [settings.serverUrl='https://{username}.carto.com'] - URL of the windshaft server. Only needed in custom installations. Pattern: `http(s)://{username}.your.carto.instance` or `http(s)://your.carto.instance/user/{username}` (only for On-Premises environments). + * + * @example + * var client = new carto.Client({ + * apiKey: 'YOUR_API_KEY_HERE', + * username: 'YOUR_USERNAME_HERE' + * }); + * + * var client = new carto.Client({ + * apiKey: 'YOUR_API_KEY_HERE', + * username: 'YOUR_USERNAME_HERE', + * serverUrl: 'http://{username}.your.carto.instance' + * }); + * + * @constructor + * @memberof carto + * @api + * + * @fires error + * @fires success + */ +function Client (settings) { + settings.serverUrl = (settings.serverUrl || DEFAULT_SERVER_URL).replace(/{username}/, settings.username || ''); + _checkSettings(settings); + this._layers = new Layers(); + this._dataviews = []; + this._engine = new Engine({ + apiKey: settings.apiKey, + username: settings.username, + serverUrl: settings.serverUrl, + client: 'js-' + VERSION + }); + this._bindEngine(this._engine); +} + +_.extend(Client.prototype, Backbone.Events); + +/** + * Add a layer to the client. + * If the layer id already exists in the client this method will throw an error. + * + * @param {carto.layer.Base} - The layer to be added + * + * @fires error + * @fires success + * + * @example + * // Add a layer to the client + * client.addLayer(layer) + * .then(() => { + * console.log('Layer added'); + * }) + * .catch(cartoError => { + * console.error(cartoError.message); + * }); + * + * @returns {Promise} - A promise that will be fulfilled when the layer is added + * @api + */ +Client.prototype.addLayer = function (layer) { + return this.addLayers([layer]); +}; + +/** + * Add multiple layers to the client at once. + * + * @param {carto.layer.Base[]} - An array with the layers to be added. Note that ([A, B]) displays B as the top layer. + * + * @fires error + * @fires success + * + * @example + * // Add multiple layers ad once layer to the client + * client.addLayers([layer0, layer1]) + * .then(() => { + * console.log('Layers added'); + * }) + * .catch(cartoError => { + * console.error(cartoError.message); + * }); + * + * @returns {Promise} A promise that will be fulfilled when the layers are added + * @api + */ +Client.prototype.addLayers = function (layers) { + layers.forEach(this._addLayer, this); + return this._reload(); +}; + +/** + * Remove a layer from the client. + * + * @example + * // Remove a layer from the client + * client.removeLayer(layer) + * .then(() => { + * console.log('Layer removed'); + * }) + * .catch(cartoError => { + * console.error(cartoError.message); + * }); + * + * @param {carto.layer.Base} - The layer to be removed + * + * @fires error + * @fires success + * + * @returns {Promise} A promise that will be fulfilled when the layer is removed + * @api + */ +Client.prototype.removeLayer = function (layer) { + return this.removeLayers([layer]); +}; + +/** + * Remove multiple layers from the client. + * + * @example + * // Remove multiple layers from the client + * client.removeLayers([layer1, layer2]) + * .then(() => { + * console.log('Layers removed'); + * }) + * .catch(cartoError => { + * console.error(cartoError.message); + * }); + * + * + * @param {carto.layer.Base[]} - An array with the layers to be removed + * + * @fires error + * @fires success + * + * @returns {Promise} A promise that will be fulfilled when the layers are removed + * @api + */ +Client.prototype.removeLayers = function (layers) { + var layersToRemove = layers.slice(0); + layersToRemove.forEach(this._removeLayer, this); + + return this._reload(); +}; + +/** + * Move layer order. + * + * @example + * // Move layer order + * client.moveLayer(layer1, 0) + * .then(() => { + * console.log('Layer moved'); + * }) + * .catch(cartoError => { + * console.error(cartoError.message); + * }); + * + * + * @param {carto.layer.Base} - The layer to be moved + * @param {number} toIndex - Final index for the layer + * + * @fires error + * @fires success + * + * @returns {Promise} A promise that will be fulfilled when the layer is moved + * @api + */ +Client.prototype.moveLayer = function (layer, toIndex) { + var fromIndex = this._layers.indexOf(layer); + this._moveLayer(layer, toIndex); + if (fromIndex === toIndex) { + return Promise.resolve(); + } else { + return this._reload(); + } +}; + +/** + * Get all the {@link carto.layer.Base|layers} from the client. + * + * @example + * // Get all layers from the client + * const layers = client.getLayers(); + * + * @example + * // Hide all layers from the client + * client.getLayers().forEach(layer => layer.hide()); + * + * @returns {carto.layer.Base[]} An array with all the Layers from the client + * @api + */ +Client.prototype.getLayers = function () { + return this._layers.toArray(); +}; + +/** + * Add a dataview to the client. + * + * @example + * // Add a dataview to the client + * client.addDataview(dataview) + * .then(() => { + * console.log('Dataview added'); + * }) + * .catch(cartoError => { + * console.error(cartoError.message); + * }): + * + * @param {carto.dataview.Base} - The dataview to be added + * + * @fires error + * @fires success + * + * @returns {Promise} - A promise that will be fulfilled when the dataview is added + * @api + */ +Client.prototype.addDataview = function (dataview) { + return this.addDataviews([dataview]); +}; + +/** + * Add multipe dataviews to the client. + * + * @example + * // Add several dataviews to the client + * client.addDataview([dataview0, dataview1]) + * .then(() => { + * console.log('Dataviews added'); + * }) + * .catch(cartoError => { + * console.error(cartoError.message); + * }): + * + * @param {carto.dataview.Base[]} - An array with the dataviews to be added + * + * @fires error + * @fires success + * + * @returns {Promise} A promise that will be fulfilled when the dataviews are added + * @api + */ +Client.prototype.addDataviews = function (dataviews) { + dataviews.forEach(this._addDataview, this); + return this._reload(); +}; + +/** + * Remove a dataview from the client. + * + * @example + * // Remove a dataview from the client + * client.removeDataview(dataview) + * .then(() => { + * console.log('Dataviews removed'); + * }) + * .catch(cartoError => { + * console.error(cartoError.message); + * }): + * + * @param {carto.dataview.Base} - The dataview array to be removed + * + * @fires error + * @fires success + * + * @returns {Promise} A promise that will be fulfilled when the dataview is removed + * @api + */ +Client.prototype.removeDataview = function (dataview) { + var dataviewIndex = this._dataviews.indexOf(dataview); + + if (dataviewIndex === -1) { + return Promise.resolve(); + } + + this._dataviews.splice(dataviewIndex, 1); + this._engine.removeDataview(dataview.$getInternalModel()); + dataview.disable(); + return this._reload(); +}; + +/** + * Get all the dataviews from the client. + * + * @example + * // Get all the dataviews from the client + * const dataviews = client.getDataviews(); + * + * @returns {carto.dataview.Base[]} An array with all the dataviews from the client + * @api + */ +Client.prototype.getDataviews = function () { + return this._dataviews; +}; + +/** + * Return a {@link http://leafletjs.com/reference-1.3.1.html#tilelayer|leaflet layer} that groups all the layers that have been + * added to this client. + * + * @example + * // Get the leafletlayer from the client + * const cartoLeafletLayer = client.getLeafletLayer(); + * + * @example + * // Add the leafletLayer to a leafletMap + * client.getLeafletLayer().addTo(map); + * + * @param {object} options - {@link https://leafletjs.com/reference-1.3.0.html#tilelayer-minzoom|L.TileLayer} options. + * + * @returns A {@link http://leafletjs.com/reference-1.3.1.html#tilelayer|L.TileLayer} layer that groups all the layers. + * + * @api + */ +Client.prototype.getLeafletLayer = function (options) { + // Check if Leaflet is loaded + utils.isLeafletLoaded(); + if (!this._leafletLayer) { + var LeafletLayer = require('./native/leaflet-layer'); + this._leafletLayer = new LeafletLayer(this._layers, this._engine, options); + } + return this._leafletLayer; +}; + +/** + * Return a {@link https://developers.google.com/maps/documentation/javascript/maptypes|google.maps.MapType} that groups all the layers that have been + * added to this client. + * + * @example + * // Get googlemaps MapType from client + * const gmapsMapType = client.getGoogleMapsMapType(); + * + * @example + * // Add googlemaps MapType to a google map + * googleMap.overlayMapTypes.push(client.getGoogleMapsMapType(googleMap)); + * + * @param {google.maps.Map} - The native Google Maps map where the CARTO layers will be displayed. + * + * @return {google.maps.MapType} A Google Maps mapType that groups all the layers: + * {@link https://developers.google.com/maps/documentation/javascript/maptypes|google.maps.MapType} + * @api + */ +Client.prototype.getGoogleMapsMapType = function (map) { + // Check if Google Maps is loaded + utils.isGoogleMapsLoaded(); + if (!this._gmapsMapType) { + var GoogleMapsMapType = require('./native/google-maps-map-type'); + this._gmapsMapType = new GoogleMapsMapType(this._layers, this._engine, map); + } + return this._gmapsMapType; +}; + +/** + * Call engine.reload wrapping the native cartojs errors + * into public CartoErrors. + */ +Client.prototype._reload = function () { + return this._engine.reload() + .then(function () { + return Promise.resolve(); + }) + .catch(function (error) { + return Promise.reject(new CartoError(error)); + }); +}; + +/** + * Helper used to link a layer and an engine. + * @private + */ +Client.prototype._addLayer = function (layer, engine) { + _checkLayer(layer); + this._checkDuplicatedLayerId(layer); + this._layers.add(layer); + layer.$setClient(this); + layer.$setEngine(this._engine); + this._engine.addLayer(layer.$getInternalModel()); +}; + +/** + * Helper used to remove a layer from the client. + * @private + */ +Client.prototype._removeLayer = function (layer) { + _checkLayer(layer); + this._layers.remove(layer); + this._engine.removeLayer(layer.$getInternalModel()); +}; + +/** + * Helper used to remove a layer from the client. + * @private + */ +Client.prototype._moveLayer = function (layer, toIndex) { + _checkLayer(layer); + _checkLayerIndex(toIndex, this._layers.size()); + this._layers.move(layer, toIndex); + this._engine.moveLayer(layer.$getInternalModel(), toIndex); +}; + +/** + * Helper used to link a dataview and an engine + * @private + */ +Client.prototype._addDataview = function (dataview, engine) { + this._dataviews.push(dataview); + dataview.$setEngine(this._engine); + this._engine.addDataview(dataview.$getInternalModel()); +}; + +/** + * Client exposes Event.SUCCESS and RELOAD_ERROR to the api users, + * those events are wrappers using _engine internaly. + */ +Client.prototype._bindEngine = function (engine) { + engine.on(Engine.Events.RELOAD_SUCCESS, function () { + this.trigger(Events.SUCCESS); + }.bind(this)); + + engine.on(Engine.Events.RELOAD_ERROR, function (err) { + this.trigger(Events.ERROR, new CartoError(err, { layers: this._layers })); + }.bind(this)); + + engine.on(Engine.Events.LAYER_ERROR, function (err) { + this.trigger(Events.ERROR, new CartoError(err)); + }.bind(this)); +}; + +/** + * Check if some layer in the client has the same id. + * @param {carto.layer.Base} layer + */ +Client.prototype._checkDuplicatedLayerId = function (layer) { + if (this._layers.findById(layer.getId())) { + throw getValidationError('duplicatedLayerId'); + } +}; + +/** + * Utility function to reduce duplicated code. + */ +function _checkLayer (layer) { + if (!(layer instanceof LayerBase)) { + throw getValidationError('badLayerType'); + } +} + +function _checkLayerIndex (index, size) { + if (!_.isNumber(index)) { + throw getValidationError('indexNumber'); + } + if (index < 0 || index >= size) { + throw getValidationError('indexOutOfRange'); + } +} + +function _checkSettings (settings) { + _checkApiKey(settings.apiKey); + _checkUsername(settings.username); + if (settings.serverUrl) { + _checkServerUrl(settings.serverUrl, settings.username); + } +} + +function _checkApiKey (apiKey) { + if (!apiKey) { + throw getValidationError('apiKeyRequired'); + } + if (!_.isString(apiKey)) { + throw getValidationError('apiKeyString'); + } +} + +function _checkUsername (username) { + if (!username) { + throw getValidationError('usernameRequired'); + } + if (!_.isString(username)) { + throw getValidationError('usernameString'); + } +} + +function _checkServerUrl (serverUrl, username) { + var urlregex = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$/; + if (!serverUrl.match(urlregex)) { + throw getValidationError('nonValidServerURL'); + } + if (serverUrl.indexOf(username) < 0) { + throw getValidationError('serverURLDoesntMatchUsername'); + } +} + +module.exports = Client; diff --git a/src/api/v4/constants.js b/src/api/v4/constants.js new file mode 100644 index 0000000..dbd8f32 --- /dev/null +++ b/src/api/v4/constants.js @@ -0,0 +1,106 @@ +var _ = require('underscore'); +/** + * Constants module for dataviews + */ + +/** + * Enum for operation values. + * + * @enum {string} carto.operation + * @readonly + * @memberof carto + * @api + */ +var operation = { + /** Number of elements */ + COUNT: 'count', + /** Sum */ + SUM: 'sum', + /** Average */ + AVG: 'avg', + /** Maximum */ + MAX: 'max', + /** Minimum */ + MIN: 'min' +}; + +function isValidOperation (op) { + return _.contains(operation, op); +} + +/** + * Enum for dataview status values. + * + * @enum {string} carto.dataview.status + * @readonly + * @memberof carto.dataview + * @api + */ +var status = { + /** Not fetched with the server */ + NOT_LOADED: 'notLoaded', + /** Fetching with the server */ + LOADING: 'loading', + /** Fetch completed */ + LOADED: 'loaded', + /** Error in fetch */ + ERROR: 'error' +}; + +/** + * Enum for dataview time aggregations. + * + * @enum {string} carto.dataview.timeAggregation + * @readonly + * @memberof carto.dataview + * @api + */ +var timeAggregation = { + /** Auto */ + AUTO: 'auto', + /** Millennium */ + MILLENNIUM: 'millennium', + /** Century */ + CENTURY: 'century', + /** Decade */ + DECADE: 'decade', + /** Year */ + YEAR: 'year', + /** Quarter */ + QUARTER: 'quarter', + /** Month */ + MONTH: 'month', + /** Week */ + WEEK: 'week', + /** Day */ + DAY: 'day', + /** Hour */ + HOUR: 'hour', + /** Minute */ + MINUTE: 'minute' +}; + +function isValidTimeAggregation (agg) { + return _.contains(timeAggregation, agg); +} + +/** + * ATTRIBUTION constant + * + * © OpenStreetMap contributors, © CARTO + * + * @type {string} + * @constant + * @memberof carto + * @api + */ +var ATTRIBUTION = '© OpenStreetMap contributors, © CARTO'; + +module.exports = { + operation: operation, + status: status, + timeAggregation: timeAggregation, + isValidOperation: isValidOperation, + isValidTimeAggregation: isValidTimeAggregation, + ATTRIBUTION: ATTRIBUTION +}; diff --git a/src/api/v4/dataview/base.js b/src/api/v4/dataview/base.js new file mode 100644 index 0000000..b5e9abb --- /dev/null +++ b/src/api/v4/dataview/base.js @@ -0,0 +1,490 @@ +var _ = require('underscore'); +var Backbone = require('backbone'); +var status = require('../constants').status; +var SourceBase = require('../source/base'); +var FilterBase = require('../filter/base'); +var SQLFilterBase = require('../filter/base-sql'); +var SpatialFilterTypes = require('../filter/spatial-filter-types'); +var CartoError = require('../error-handling/carto-error'); +var CartoValidationError = require('../error-handling/carto-validation-error'); + +/** + * Base class for dataview objects. + * + * Dataviews are a way to extract data from a CARTO account in predefined ways + * (eg: a list of categories, the result of a formula operation, etc.). + * + * **This object should not be used directly** + * + * The data used in a dataviews cames from a {@link carto.source.Base|source} that might change + * due to different reasons (eg: SQL query changed). + * + * When dataview data changes the dataview will trigger events to notify subscribers when new data is available. + * + * @example + * // Keep your widget data sync. Remember each dataview has his own data format. + * dataview.on('dataChanged', newData => { + * renderWidget(newData); + * }) + * + * @constructor + * @abstract + * @memberof carto.dataview + * @fires dataChanged + * @fires columnChanged + * @fires statusChanged + * @fires error + * @api + */ +function Base () { } + +_.extend(Base.prototype, Backbone.Events); + +/** + * Return the current dataview status. + * + * @return {carto.dataview.status} Current dataview status + * @api + */ +Base.prototype.getStatus = function () { + return this._status; +}; + +/** + * Return true is the current status is loading. + * + * @return {boolean} + * @api + */ +Base.prototype.isLoading = function () { + return this._status === status.LOADING; +}; + +/** + * Return true is the current status is loaded. + * + * @return {boolean} + * @api + */ +Base.prototype.isLoaded = function () { + return this._status === status.LOADED; +}; + +/** + * Return true is the current status is error. + * + * @return {boolean} + * @api + */ +Base.prototype.hasError = function () { + return this._status === status.ERROR; +}; + +/** + * Enable the dataview. When enabled, a dataview fetches new data + * when the map changes (changing map configuration or changing map + * bounding box). + * + * @return {carto.dataview.Base} this + * @api + */ +Base.prototype.enable = function () { + return this._setEnabled(true); +}; + +/** + * Disable the dataview. This stops the dataview from fetching new + * data when there is a map change (like changing map configuration or changing map + * bounding box). + * + * @return {carto.dataview.Base} this + * @api + */ +Base.prototype.disable = function () { + return this._setEnabled(false); +}; + +/** + * Return true if the dataview is enabled. + * + * @return {boolean} + * @api + */ +Base.prototype.isEnabled = function () { + return this._enabled; +}; + +/** + * Return the current source where the dataview gets the data from. + * + * @return {carto.source.Base} Current source object + * @api + */ +Base.prototype.getSource = function () { + return this._source; +}; + +/** + * Set the dataview column. + * + * @param {string} column + * @fires columnChanged + * @return {carto.dataview.Base} this + * @api + */ +Base.prototype.setColumn = function (column) { + this._checkColumn(column); + this._column = column; + if (this._internalModel) { + this._internalModel.set('column', this._column); + } + return this; +}; + +/** + * Return the current dataview column where the dataview is applied. + * + * @return {string} Current dataview column + * @api + */ +Base.prototype.getColumn = function () { + return this._column; +}; + +/** + * Add a {@link carto.filter.Base|filter}. + * + * @param {carto.filter.Base} filter + * @return {carto.dataview.Base} this + * @api + */ +Base.prototype.addFilter = function (filter) { + this._checkFilter(filter); + this._addSpatialFilter(filter); + return this; +}; + +/** + * Remove a {@link carto.filter.Base|filter}. + * + * @param {carto.filter.Base} filter + * @return {carto.dataview.Base} this + * @api + */ +Base.prototype.removeFilter = function (filter) { + this._checkFilter(filter); + this._removeSpatialFilter(filter); + return this; +}; + +/** + * Check if a {@link carto.filter.Base|filter} exists in the dataview. + * + * @param {carto.filter.Base} filter + * @return {carto.dataview.Base} this + * @api + */ +Base.prototype.hasFilter = function (filter) { + this._checkFilter(filter); + var hasBBoxFilter = (filter === this._boundingBoxFilter) && + (this._internalModel && this._internalModel.get('sync_on_bbox_change')); + + var hasCircleFilter = (filter === this._circleFilter) && + (this._internalModel && this._internalModel.get('sync_on_circle_change')); + + var hasPolygonFilter = (filter === this._polygonFilter) && + (this._internalModel && this._internalModel.get('sync_on_polygon_change')); + + return hasBBoxFilter || hasCircleFilter || hasPolygonFilter; +}; + +Base.prototype.getData = function () { + throw new Error('getData must be implemented by the particular dataview.'); +}; + +// Protected methods + +Base.prototype.DEFAULTS = {}; + +/** + * Initialize dataview. + * + * @param {carto.source.Base} source - The source where the dataview will fetch the data + * @param {string} column - The column name to get the data + * @param {object} options - It depends on the instance + */ +Base.prototype._initialize = function (source, column, options) { + options = _.defaults(options || {}, this.DEFAULTS); + + this._checkSource(source); + this._checkColumn(column); + this._checkOptions(options); + + this._source = source; + this._column = column; + this._options = options; + + this._status = status.NOT_LOADED; + this._enabled = true; + this._boundingBoxFilter = null; +}; + +Base.prototype._checkSource = function (source) { + if (!(source instanceof SourceBase)) { + throw this._getValidationError('sourceRequired'); + } +}; + +Base.prototype._checkColumn = function (column) { + if (_.isUndefined(column)) { + throw this._getValidationError('columnRequired'); + } + if (!_.isString(column)) { + throw this._getValidationError('columnString'); + } + if (_.isEmpty(column)) { + throw this._getValidationError('emptyColumn'); + } +}; + +Base.prototype._checkOptions = function (options) { + throw new Error('_checkOptions must be implemented by the particular dataview.'); +}; + +Base.prototype._checkFilter = function (filter) { + if (!(filter instanceof FilterBase) || filter instanceof SQLFilterBase) { + throw this._getValidationError('filterRequired'); + } +}; + +Base.prototype._createInternalModel = function (engine) { + throw new Error('_createInternalModel must be implemented by the particular dataview.'); +}; + +Base.prototype._setEnabled = function (enabled) { + this._enabled = enabled; + if (this._internalModel) { + this._internalModel.set('enabled', enabled); + } + return this; +}; + +Base.prototype._listenToInternalModelSharedEvents = function () { + if (this._internalModel) { + this.listenTo(this._internalModel, 'change:data', this._onDataChanged); + this.listenTo(this._internalModel, 'change:column', this._onColumnChanged); + this.listenTo(this._internalModel, 'loading', this._onStatusLoading); + this.listenTo(this._internalModel, 'loaded', this._onStatusLoaded); + this.listenTo(this._internalModel, 'statusError', this._onStatusError); + } +}; + +Base.prototype._onDataChanged = function () { + this.trigger('dataChanged', this.getData()); +}; + +Base.prototype._onColumnChanged = function () { + if (this._internalModel) { + this._column = this._internalModel.get('column'); + } + this.trigger('columnChanged', this._column); +}; + +Base.prototype._onStatusLoading = function () { + this._status = status.LOADING; + this.trigger('statusChanged', this._status); +}; + +Base.prototype._onStatusLoaded = function () { + this._status = status.LOADED; + this.trigger('statusChanged', this._status); +}; + +Base.prototype._onStatusError = function (model, error) { + this._status = status.ERROR; + this.trigger('statusChanged', this._status, error); + this._triggerError(this, error); +}; + +Base.prototype._changeProperty = function (key, value, internalKey) { + var prevValue = this['_' + key]; + this['_' + key] = value; + if (prevValue === value) { + return; + } + this._triggerChange(key, value); + if (this._internalModel) { + this._internalModel.set(internalKey || key, value); + } +}; + +Base.prototype._changeProperties = function (properties) { + _.each(properties, (value, key) => { + const prevValue = this[`_${key}`]; + + if (prevValue !== value) { + this[`_${key}`] = value; + this._triggerChange(key, value); + } + }); + + if (this._internalModel) { + this._internalModel.set(properties); + } +}; + +Base.prototype._triggerChange = function (key, value) { + this.trigger(key + 'Changed', value); +}; + +/** + * Fire a CartoError event from a internalDataviewError. + */ +Base.prototype._triggerError = function (model, internalDataviewError) { + this.trigger('error', new CartoError(internalDataviewError)); +}; + +Base.prototype._addSpatialFilter = function (spatialFilter) { + switch (spatialFilter.type) { + case SpatialFilterTypes.BBOX: + this._addBoundingBoxFilter(spatialFilter); + break; + case SpatialFilterTypes.CIRCLE: + this._addCircleFilter(spatialFilter); + break; + case SpatialFilterTypes.POLYGON: + this._addPolygonFilter(spatialFilter); + break; + default: + throw new Error('The filter is not a valid spatial filter.'); + } +}; + +Base.prototype._removeSpatialFilter = function (spatialFilter) { + switch (spatialFilter.type) { + case SpatialFilterTypes.BBOX: + if (spatialFilter === this._boundingBoxFilter) { + this._removeBoundingBoxFilter(); + } + break; + case SpatialFilterTypes.CIRCLE: + if (spatialFilter === this._circleFilter) { + this._removeCircleFilter(); + } + break; + case SpatialFilterTypes.POLYGON: + if (spatialFilter === this._polygonFilter) { + this._removePolygonFilter(); + } + break; + default: + throw new Error('The filter is not a valid spatial filter.'); + } +}; + +Base.prototype._addBoundingBoxFilter = function (bboxFilter) { + if (bboxFilter === this._boundingBoxFilter) { + return; + } + + this._boundingBoxFilter = bboxFilter; + if (this._internalModel) { + this._internalModel.addBBoxFilter(this._boundingBoxFilter.$getInternalModel()); + this._internalModel.set('sync_on_bbox_change', true); + } +}; + +Base.prototype._removeBoundingBoxFilter = function () { + this._boundingBoxFilter = null; + if (this._internalModel) { + this._internalModel.removeBBoxFilter(); + this._internalModel.set('sync_on_bbox_change', false); + } +}; + +Base.prototype._addCircleFilter = function (circleFilter) { + if (circleFilter === this._circleFilter) { + return; + } + + this._circleFilter = circleFilter; + if (this._internalModel) { + this._internalModel.addCircleFilter(this._circleFilter.$getInternalModel()); + this._internalModel.set('sync_on_circle_change', true); + } +}; + +Base.prototype._removeCircleFilter = function () { + this._circleFilter = null; + if (this._internalModel) { + this._internalModel.removeCircleFilter(); + this._internalModel.set('sync_on_circle_change', false); + } +}; + +Base.prototype._addPolygonFilter = function (polygonFilter) { + if (polygonFilter === this._polygonFilter) { + return; + } + + this._polygonFilter = polygonFilter; + if (this._internalModel) { + this._internalModel.addPolygonFilter(this._polygonFilter.$getInternalModel()); + this._internalModel.set('sync_on_polygon_change', true); + } +}; + +Base.prototype._removePolygonFilter = function () { + this._polygonFilter = null; + if (this._internalModel) { + this._internalModel.removePolygonFilter(); + this._internalModel.set('sync_on_polygon_change', false); + } +}; + +Base.prototype._getValidationError = function (code) { + return new CartoValidationError('dataview', code); +}; + +// Internal public methods + +Base.prototype.$setEngine = function (engine) { + this._source.$setEngine(engine); + if (!this._internalModel) { + this._createInternalModel(engine); + this._listenToInternalModelSharedEvents(); + } +}; + +Base.prototype.$getInternalModel = function () { + return this._internalModel; +}; + +module.exports = Base; + +/** + * Fired when the column name has changed. Handler gets a parameter with the new column name. + * + * @event columnChanged + * @type {string} + * @api + */ + +/** + * Fired when the status has changed. Handler gets a parameter with the new status. + * + * Contains a single argument with the new status. + * + * @event statusChanged + * @type {carto.dataview.status} + * @api + */ + +/** + * Fired when the data has changed. Handler gets an object with specific data for the type + * of dataview that triggered the event. + * + * @event dataChanged + * @type {carto.dataview.CategoryData|carto.dataview.FormulaData|carto.dataview.HistogramData|carto.dataview.TimeSeriesData} + * @api + */ diff --git a/src/api/v4/dataview/category/index.js b/src/api/v4/dataview/category/index.js new file mode 100644 index 0000000..508983e --- /dev/null +++ b/src/api/v4/dataview/category/index.js @@ -0,0 +1,268 @@ +var _ = require('underscore'); +var Base = require('../base'); +var constants = require('../../constants'); +var CategoryDataviewModel = require('../../../../dataviews/category-dataview-model'); +var CategoryFilter = require('../../../../windshaft/filters/category'); +var parseCategoryData = require('./parse-data.js'); + +/** + * + * A category dataview is used to aggregate data performing a operation. + * + * This is similar to a group by SQL operation, for example: + * + * ``` + * SELECT country, AVG(population) GROUP BY country + * ``` + * The following code is the CARTO.js equivalent: + * + * ```javascript + * var categoryDataview = new carto.dataview.Category(citiesSource, 'country', { + * operation: carto.operation.AVG, // Compute the average + * operationColumn: 'population' // The name of the column where the operation will be applied. + * }); + * ``` + * + * Like every other dataview, this is an async object and you must wait for the data to be available. + * + * The data format for the category-dataview is described in {@link carto.dataview.CategoryData} + * + * @param {carto.source.Base} source - The source where the dataview will fetch the data + * @param {string} column - The name of the column used to create categories + * @param {object} [options] + * @param {number} [options.limit=6] - The maximum number of categories in the response + * @param {carto.operation} [options.operation] - The operation to apply to the data + * @param {string} [options.operationColumn] - The column where the operation will be applied + * + * @fires dataChanged + * @fires columnChanged + * @fires statusChanged + * @fires error + * + * @fires limitChanged + * @fires operationChanged + * @fires operationColumnChanged + * + * @constructor + * @extends carto.dataview.Base + * @memberof carto.dataview + * @api + * @example + * // From a cities dataset with name, country and population show the average city population per country: + * var column = 'country'; // Aggregate the data by country. + * var categoryDataview = new carto.dataview.Category(citiesSource, column, { + * operation: carto.operation.AVG, // Compute the average + * operationColumn: 'population' // The name of the column where the operation will be applied. + * }); + * @example + * // Listen for data updates + * categoryDataview.on('dataChanged', newData => { + * console.log(newData); // CategoryData object + * }); + * @example + * // You can listen to multiple events emmited by the category-dataview. + * categoryDataview.on('statusChanged', (newData, error) => { }); + * categoryDataview.on('error', cartoError => { }); + * + * // Listen to specific category-dataview events. + * categoryDataview.on('columnChanged', newData => { + * console.log(newData); // 'population' + * }); + * categoryDataview.on('limitChanged', newData => { + * console.log(newData); // 11 + * }); + * categoryDataview.on('operationChanged', newData => { }); + * categoryDataview.on('operationColumnChanged', newData => { }); + */ +function Category (source, column, options) { + this.DEFAULTS.operationColumn = column; + + this._initialize(source, column, options); + this._limit = this._options.limit; + this._operation = this._options.operation; + this._operationColumn = this._options.operationColumn; +} + +Category.prototype = Object.create(Base.prototype); + +/** + * Set the categories limit. + * + * @param {number} limit + * @fires limitChanged + * @return {carto.dataview.Category} this + * @api + */ +Category.prototype.setLimit = function (limit) { + this._checkLimit(limit); + this._changeProperty('limit', limit, 'categories'); + return this; +}; + +/** + * Return the current categories limit. + * + * @return {number} Current dataview limit + * @api + */ +Category.prototype.getLimit = function () { + return this._limit; +}; + +/** + * Set the dataview operation. + * + * @param {carto.operation} operation + * @fires operationChanged + * @return {carto.dataview.Category} this + * @api + */ +Category.prototype.setOperation = function (operation) { + this._checkOperation(operation); + this._changeProperty('operation', operation, 'aggregation'); + return this; +}; + +/** + * Return the current dataview operation. + * + * @return {carto.operation} Current dataview operation + * @api + */ +Category.prototype.getOperation = function () { + return this._operation; +}; + +/** + * Set the dataview operationColumn. + * + * @param {string} operationColumn + * @fires operationColumnChanged + * @return {carto.dataview.Category} this + * @api + */ +Category.prototype.setOperationColumn = function (operationColumn) { + this._checkOperationColumn(operationColumn); + this._changeProperty('operationColumn', operationColumn, 'aggregation_column'); + return this; +}; + +/** + * Return the current dataview operationColumn. + * + * @return {string} Current dataview operationColumn + * @api + */ +Category.prototype.getOperationColumn = function () { + return this._operationColumn; +}; + +/** + * Return the resulting data. + * + * @return {carto.dataview.CategoryData} + * @api + */ +Category.prototype.getData = function () { + if (this._internalModel) { + return parseCategoryData( + this._internalModel.get('data'), + this._internalModel.get('count'), + this._internalModel.get('max'), + this._internalModel.get('min'), + this._internalModel.get('nulls'), + this._operation + ); + } + return null; +}; + +Category.prototype.DEFAULTS = { + limit: 6, + operation: constants.operation.COUNT +}; + +Category.prototype._checkOptions = function (options) { + if (_.isUndefined(options)) { + throw this._getValidationError('categoryOptionsRequired'); + } + this._checkLimit(options.limit); + this._checkOperation(options.operation); + this._checkOperationColumn(options.operationColumn); +}; + +Category.prototype._checkLimit = function (limit) { + if (_.isUndefined(limit)) { + throw this._getValidationError('categoryLimitRequired'); + } + if (!_.isNumber(limit)) { + throw this._getValidationError('categoryLimitNumber'); + } + if (limit <= 0) { + throw this._getValidationError('categoryLimitPositive'); + } +}; + +Category.prototype._checkOperation = function (operation) { + if (_.isUndefined(operation) || !constants.isValidOperation(operation)) { + throw this._getValidationError('categoryInvalidOperation'); + } +}; + +Category.prototype._checkOperationColumn = function (operationColumn) { + if (_.isUndefined(operationColumn)) { + throw this._getValidationError('categoryOperationRequired'); + } + if (!_.isString(operationColumn)) { + throw this._getValidationError('categoryOperationString'); + } + if (_.isEmpty(operationColumn)) { + throw this._getValidationError('categoryOperationEmpty'); + } +}; + +Category.prototype._createInternalModel = function (engine) { + this._internalModel = new CategoryDataviewModel({ + source: this._source.$getInternalModel(), + column: this._column, + aggregation: this._operation, + aggregation_column: this._operationColumn, + categories: this._limit, + sync_on_bbox_change: !!this._boundingBoxFilter, + sync_on_circle_change: !!this._circleFilter, + sync_on_polygon_change: !!this._polygonFilter, + enabled: this._enabled + }, { + engine: engine, + filter: new CategoryFilter(), + bboxFilter: this._boundingBoxFilter && this._boundingBoxFilter.$getInternalModel(), + circleFilter: this._circleFilter && this._circleFilter.$getInternalModel(), + polygonFilter: this._polygonFilter && this._polygonFilter.$getInternalModel() + }); +}; + +module.exports = Category; + +/** + * Fired when limit has changed. Handler gets a parameter with the new limit. + * + * @event limitChanged + * @type {number} + * @api + */ + +/** + * Fired when operation has changed. Handler gets a parameter with the new limit. + * + * @event operationChanged + * @type {string} + * @api + */ + +/** + * Fired when operationColumn has changed. Handler gets a parameter with the new operationColumn. + * + * @event operationColumnChanged + * @type {string} + * @api + */ diff --git a/src/api/v4/dataview/category/parse-data.js b/src/api/v4/dataview/category/parse-data.js new file mode 100644 index 0000000..5f08960 --- /dev/null +++ b/src/api/v4/dataview/category/parse-data.js @@ -0,0 +1,60 @@ +var _ = require('underscore'); + +/** + * Transform the data obtained from an internal category dataview into a + * public object. + * + * @param {object[]} data + * @param {number} count + * @param {number} max + * @param {number} min + * @param {number} nulls + * @param {string} operation + * + * @return {carto.dataview.CategoryData} - The parsed and formatted data for the given parameters + */ +function parseCategoryData (data, count, max, min, nulls, operation) { + if (!data) { + return null; + } + /** + * @typedef {object} carto.dataview.CategoryData + * @property {number} count - The total number of categories + * @property {number} max - Maximum category value + * @property {number} min - Minimum category value + * @property {number} nulls - Number of null categories + * @property {string} operation - Operation used + * @property {carto.dataview.CategoryItem[]} categories + * @api + */ + return { + count: count, + max: max, + min: min, + nulls: nulls, + operation: operation, + categories: _createCategories(data) + }; +} + +/** + * Transform the histogram raw data into {@link carto.dataview.CategoryItem} + */ +function _createCategories (data) { + return _.map(data, function (item) { + /** + * @typedef {object} carto.dataview.CategoryItem + * @property {boolean} group - Category is a group + * @property {string} name - Category name + * @property {number} value - Category value + * @api + */ + return { + group: item.agg, + name: item.name, + value: item.value + }; + }); +} + +module.exports = parseCategoryData; diff --git a/src/api/v4/dataview/formula/index.js b/src/api/v4/dataview/formula/index.js new file mode 100644 index 0000000..3b43e6b --- /dev/null +++ b/src/api/v4/dataview/formula/index.js @@ -0,0 +1,134 @@ +var _ = require('underscore'); +var Base = require('../base'); +var constants = require('../../constants'); +var FormulaDataviewModel = require('../../../../dataviews/formula-dataview-model'); +var parseFormulaData = require('./parse-data.js'); + +/** + * A formula is a simple numeric {@link carto.operation|operation} applied to the column of a {@link carto.source.Base|data source} (dataset or sql query). + * + * Like all dataviews, it is an async object so you must wait for the data to be available. + * + * @param {carto.source.Base} source - The source where the dataview will fetch the data from + * @param {string} column - The operation will be performed using this column + * @param {object} [options] + * @param {carto.operation} [options.operation] - The operation to apply to the data + * + * @fires dataChanged + * @fires columnChanged + * @fires statusChanged + * @fires error + * + * @fires operationChanged + * + * @constructor + * @extends carto.dataview.Base + * @memberof carto.dataview + * @api + * @example + * // Given a cities dataset get the most populated city + * var formulaDataview = new carto.dataview.Formula(citiesSource, 'population', { + * operation: carto.operation.MAX, + * }); + * @example + * // You can listen to multiple events emitted by a formula dataview. + * // Data and status are fired by all dataviews. + * formulaDataview.on('dataChanged', newData => { }); + * formulaDataview.on('statusChanged', (newData, error) => { }); + * formulaDataview.on('error', cartoError => { }); + * + * // Listen to specific formula-dataview events + * formulaDataview.on('columnChanged', newData => { }); + * formulaDataview.on('operationChanged', newData => { }); + */ +function Formula (source, column, options) { + this._initialize(source, column, options); + this._operation = this._options.operation; +} + +Formula.prototype = Object.create(Base.prototype); + +/** + * Set the dataview operation. + * + * @param {carto.operation} operation + * @fires operationChanged + * @return {carto.dataview.Formula} this + * @api + */ +Formula.prototype.setOperation = function (operation) { + this._checkOperation(operation); + this._changeProperty('operation', operation); + return this; +}; + +/** + * Return the current dataview operation. + * + * @return {carto.operation} Current dataview operation + * @api + */ +Formula.prototype.getOperation = function () { + return this._operation; +}; + +/** + * Return the resulting data. + * + * @return {carto.dataview.FormulaData} + * @api + */ +Formula.prototype.getData = function () { + if (this._internalModel) { + return parseFormulaData( + this._internalModel.get('nulls'), + this._operation, + this._internalModel.get('data') + ); + } + return null; +}; + +Formula.prototype.DEFAULTS = { + operation: constants.operation.COUNT +}; + +Formula.prototype._checkOptions = function (options) { + if (_.isUndefined(options)) { + throw this._getValidationError('formulaOptionsRequired'); + } + this._checkOperation(options.operation); +}; + +Formula.prototype._checkOperation = function (operation) { + if (_.isUndefined(operation) || !constants.isValidOperation(operation)) { + throw this._getValidationError('formulaInvalidOperation'); + } +}; + +Formula.prototype._createInternalModel = function (engine) { + this._internalModel = new FormulaDataviewModel({ + source: this._source.$getInternalModel(), + column: this._column, + operation: this._operation, + sync_on_bbox_change: !!this._boundingBoxFilter, + sync_on_circle_change: !!this._circleFilter, + sync_on_polygon_change: !!this._polygonFilter, + enabled: this._enabled + }, { + engine: engine, + bboxFilter: this._boundingBoxFilter && this._boundingBoxFilter.$getInternalModel(), + circleFilter: this._circleFilter && this._circleFilter.$getInternalModel(), + polygonFilter: this._polygonFilter && this._polygonFilter.$getInternalModel() + }); +}; + +module.exports = Formula; + +/** + * Fired when the operation has changed. Handler gets a parameter with the new operation. + * + * @event operationChanged + * @type {carto.operation} + * @api + */ diff --git a/src/api/v4/dataview/formula/parse-data.js b/src/api/v4/dataview/formula/parse-data.js new file mode 100644 index 0000000..ae08079 --- /dev/null +++ b/src/api/v4/dataview/formula/parse-data.js @@ -0,0 +1,29 @@ +/** + * Transform the data obtained from an internal formula dataview into a + * public object. + * + * @param {number} nulls + * @param {string} operation + * @param {number} result + * + * @return {carto.dataview.FormulaData} - The parsed and formatted data for the given parameters + */ +function parseFormulaData (nulls, operation, result) { + /** + * @description + * Object containing formula data + * + * @typedef {object} carto.dataview.FormulaData + * @property {number} nulls - Number of null values in the column + * @property {string} operation - Operation used + * @property {number} result - Result of the operation + * @api + */ + return { + nulls: nulls, + operation: operation, + result: result + }; +} + +module.exports = parseFormulaData; diff --git a/src/api/v4/dataview/histogram/index.js b/src/api/v4/dataview/histogram/index.js new file mode 100644 index 0000000..011307e --- /dev/null +++ b/src/api/v4/dataview/histogram/index.js @@ -0,0 +1,228 @@ +var _ = require('underscore'); +var Base = require('../base'); +var HistogramDataviewModel = require('../../../../dataviews/histogram-dataview-model'); +var parseHistogramData = require('./parse-data.js'); + +/** + * A histogram is used to represent the distribution of numerical data. + * + * See {@link https://en.wikipedia.org/wiki/Histogram}. + * + * @param {carto.source.Base} source - The source where the dataview will fetch the data + * @param {string} column - The column name to get the data + * @param {object} [options] + * @param {number} [options.bins=10] - Number of bins to aggregate the data range into + * @param {number} [options.start] - Lower limit of the data range, if not present, the lower limit of the actual data will be used. Start and end values must be used together. + * @param {number} [options.end] - Upper limit of the data range, if not present, the upper limit of the actual data will be used. Start and end values must be used together. + * + * @fires dataChanged + * @fires columnChanged + * @fires statusChanged + * @fires error + * + * @fires binsChanged + * + * @constructor + * @extends carto.dataview.Base + * @memberof carto.dataview + * @api + * @example + * // Create a cities population histogram. + * var histogram = new carto.dataview.Histogram(citiesSource, 'population'); + * // Set up a callback to render the histogram data every time new data is obtained. + * histogram.on('dataChanged', renderData); + * // Add the histogram to the client + * client.addDataview(histogram); + * @example + * // Create a cities population histogram with only 4 bins + * var histogram = new carto.dataview.Histogram(citiesSource, 'population', {bins: 4}); + * // Add a bounding box filter, so the data will change when the map is moved. + * var bboxFilter = new carto.filter.BoundingBoxLeaflet(map); + * // Set up a callback to render the histogram data every time new data is obtained. + * histogram.on('dataChanged', histogramData => { + * console.log(histogramData); + * }); + * // Add the histogram to the client + * client.addDataview(histogram); + * @example + * // Create a cities population histogram with a range + * var histogram = new carto.dataview.Histogram(citiesSource, 'population', { start: 100000, end: 5000000 }); + * // Set up a callback to render the histogram data every time new data is obtained. + * histogram.on('dataChanged', histogramData => { + * console.log(histogramData); + * }); + * // Add the histogram to the client + * client.addDataview(histogram); + * @example + * // The histogram is an async object so it can be on different states: LOADING, ERROR... + * // Listen to state events + * histogram.on('statusChanged', (newStatus, error) => { }); + * // Listen to histogram errors + * histogram.on('error', error => { }); + */ +function Histogram (source, column, options) { + this._initialize(source, column, options); + this._bins = this._options.bins; + this._start = this._options.start; + this._end = this._options.end; +} + +Histogram.prototype = Object.create(Base.prototype); + +Histogram.prototype.DEFAULTS = { + bins: 10 +}; + +/** + * Return the resulting data. + * + * @return {carto.dataview.HistogramData} + * @api + */ +Histogram.prototype.getData = function () { + if (this._internalModel) { + return parseHistogramData( + this._internalModel.get('data'), + this._internalModel.get('nulls'), + this._internalModel.get('totalAmount') + ); + } + return null; +}; + +Histogram.prototype.setColumn = function (column) { + Base.prototype.setColumn.apply(this, arguments); + this._start = null; + this._end = null; +}; + +/** + * Set the number of bins. + * + * @param {number} bins + * @fires binsChanged + * @return {carto.dataview.Histogram} this + * @api + */ +Histogram.prototype.setBins = function (bins) { + this._validateBins(bins); + this._changeProperty('bins', bins); + return this; +}; + +/** + * Return the current number of bins. + * + * @return {number} Current number of bins + * @api + */ +Histogram.prototype.getBins = function () { + return this._bins; +}; + +/** + * Set the lower and upper limit of the bins range + * + * @param {number} start + * @param {number} end + * @return {carto.dataview.Histogram} this + * @api + */ +Histogram.prototype.setStartEnd = function (start, end) { + this._validateStartEnd(start, end); + + this._changeProperties({ start, end }); + + return this; +}; + +/** + * Return the lower limit of the bins' range + * + * @return {number} Current value of start + * @api + */ +Histogram.prototype.getStart = function () { + return this._start || this._internalModel.get('start'); +}; + +/** + * Return the upper limit of the bins' range + * + * @return {number} Current value of end + * @api + */ +Histogram.prototype.getEnd = function () { + return this._end || this._internalModel.get('end'); +}; + +/** + * Return the distribution type of the current data according to [Galtung’s AJUS System]{@link https://en.wikipedia.org/wiki/Multimodal_distribution#Galtung.27s_classification} + * + * @return {string} Distribution type of current data + * @api + */ +Histogram.prototype.getDistributionType = function () { + if (this._internalModel) { + var data = this._internalModel.getData(); + return this._internalModel.getDistributionType(data); + } + return null; +}; + +Histogram.prototype._validateBins = function (bins) { + if (!_.isFinite(bins) || bins < 1 || Math.floor(bins) !== bins) { + throw this._getValidationError('histogramInvalidBins'); + } +}; + +Histogram.prototype._validateStartEnd = function (start, end) { + const values = [start, end]; + + if (_.every(values, _.isUndefined)) return; + + const bothAreNumbers = _.every(values, number => _.isNumber(number) && !_.isNaN(number)); + const bothAreNull = _.every(values, _.isNull); + + if (!bothAreNumbers && !bothAreNull) { + throw this._getValidationError('histogramInvalidStartEnd'); + } +}; + +Histogram.prototype._checkOptions = function (options) { + if (_.isUndefined(options)) { + throw this._getValidationError('histogramOptionsRequired'); + } + this._validateBins(options.bins); + this._validateStartEnd(options.start, options.end); +}; + +Histogram.prototype._createInternalModel = function (engine) { + this._internalModel = new HistogramDataviewModel({ + source: this._source.$getInternalModel(), + column: this._column, + bins: this._bins, + start: this._start, + end: this._end, + sync_on_bbox_change: !!this._boundingBoxFilter, + sync_on_circle_change: !!this._circleFilter, + sync_on_polygon_change: !!this._polygonFilter, + enabled: this._enabled, + column_type: 'number' + }, { + engine: engine, + bboxFilter: this._boundingBoxFilter && this._boundingBoxFilter.$getInternalModel(), + circleFilter: this._circleFilter && this._circleFilter.$getInternalModel(), + polygonFilter: this._polygonFilter && this._polygonFilter.$getInternalModel() + }); +}; + +module.exports = Histogram; + +/** + * Fired when bins have changed. Handler gets a parameter with the new bins. + * + * @event binsChanged + * @type {number} + * @api + */ diff --git a/src/api/v4/dataview/histogram/parse-data.js b/src/api/v4/dataview/histogram/parse-data.js new file mode 100644 index 0000000..35e4809 --- /dev/null +++ b/src/api/v4/dataview/histogram/parse-data.js @@ -0,0 +1,91 @@ +var _ = require('underscore'); + +/** + * Transform the data obtained from an internal histogram dataview into a + * public object. + * + * @param {object[]} data - The raw histogram data + * @param {number} nulls - Number of data with a null + * @param {number} totalAmount - Total number of data in the histogram + * + * @return {carto.dataview.HistogramData} - The parsed and formatted data for the given parameters + */ +function parseHistogramData (data, nulls, totalAmount) { + if (!data) { + return null; + } + var compactData = _.compact(data); + var maxBin = _.max(compactData, function (bin) { return bin.freq || 0; }); + var maxFreq = _.isFinite(maxBin.freq) && maxBin.freq !== 0 + ? maxBin.freq + : null; + + /** + * @description + * Object containing histogram data. + * + * @typedef {object} carto.dataview.HistogramData + * @property {number} nulls - The number of items with null value + * @property {number} totalAmount - The number of elements returned + * @property {carto.dataview.BinItem[]} bins - Array containing the {@link carto.dataview.BinItem|data bins} for the histogram + * @property {string} type - String with value: **histogram** + * @api + */ + return { + bins: _createBins(compactData, maxFreq), + nulls: nulls || 0, + totalAmount: totalAmount + }; +} + +/** + * Transform the histogram raw data into {@link carto.dataview.BinItem} + */ +function _createBins (data, maxFreq) { + return data.map(function (bin) { + /** + * @example + * + * // We created an histogram containing airBnb prices per night + * const histogramDataview = new carto.dataview.Histogram(airbnbDataset, 'price', { bins: 7 }); + * // Listen to dataChanged events + * histogramDataview.on('dataChanged', data => { + * // The first bin contains prices from 0 to 20€ per night, there are 3 rentals in this bin with a cost of 10 15 and 20€. + * const bin = console.log(data.bins[0]); + * // This is the bin index in the bins array + * bin.index; // 0 + * // The first bin contains rentals from 0 to 20€ per night + * bin.start; // 0 + * // The first bin contains rentals from 0 to 20€ per night + * bin.end; // 20 + * // The lower rental in the bin is 10€ per night + * bin.min; // 10 + * // The maximun rental in the bin is 20€ per night + * bin.max; // 20 + * // The average price in this bin is 15€ per night + * bin.avg; // 15 + * // The bin contains 3 prices + * bin.freq; // 3 + * // Those 3 prices represent the 20% of the dataset. + * bin.normalized; // 0.2 + * }); + * + * + * + * + * @typedef {object} carto.dataview.BinItem + * @property {number} index - Number indicating the bin order + * @property {number} start - The lower limit of the bin + * @property {number} end - The higher limit of the bin + * @property {number} min - The minimal value appearing in the bin. Only appears if freq > 0 + * @property {number} max - The minimal value appearing in the bin. Only appears if freq > 0 + * @property {number} avg - The average value of the elements for this bin. Only appears if freq > 0 + * @property {number} freq - Number of elements in the bin + * @property {number} normalized - Normalized frequency with respect to the whole data + * @api + */ + return _.extend(bin, { normalized: _.isFinite(bin.freq) && maxFreq > 0 ? bin.freq / maxFreq : 0 }); + }); +} + +module.exports = parseHistogramData; diff --git a/src/api/v4/dataview/index.js b/src/api/v4/dataview/index.js new file mode 100644 index 0000000..4192616 --- /dev/null +++ b/src/api/v4/dataview/index.js @@ -0,0 +1,19 @@ +var Category = require('./category'); +var Formula = require('./formula'); +var Histogram = require('./histogram'); +var TimeSeries = require('./time-series'); +var status = require('../constants').status; +var timeAggregation = require('../constants').timeAggregation; + +/** + * @namespace carto.dataview + * @api + */ +module.exports = { + Category: Category, + Formula: Formula, + Histogram: Histogram, + TimeSeries: TimeSeries, + status: status, + timeAggregation: timeAggregation +}; diff --git a/src/api/v4/dataview/time-series/index.js b/src/api/v4/dataview/time-series/index.js new file mode 100644 index 0000000..5176163 --- /dev/null +++ b/src/api/v4/dataview/time-series/index.js @@ -0,0 +1,230 @@ +var _ = require('underscore'); +var Base = require('../base'); +var HistogramDataviewModel = require('../../../../dataviews/histogram-dataview-model'); +var parseTimeSeriesData = require('./parse-data'); +var timeAggregation = require('../../constants').timeAggregation; +var isValidTimeAggregation = require('../../constants').isValidTimeAggregation; + +/** + * A dataview to represent an histogram of temporal data allowing to specify the granularity of the {@link carto.dataview.timeAggregation|temporal bins.} + * + * @param {carto.source.Base} source - The source where the dataview will fetch the data + * @param {string} column - The column name to get the data + * @param {object} [options] + * @param {carto.dataview.timeAggregation} [options.aggregation=auto] - Granularity of time aggregation + * @param {number} [options.offset] - Number of hours to offset the aggregation from UTC + * @param {boolean} [options.useLocalTimezone] - Indicates whether to use the local user timezone, or not + * + * @fires dataChanged + * @fires columnChanged + * @fires statusChanged + * @fires error + * + * @fires binsChanged + * @fires aggregationChanged + * @fires offsetChanged + * @fires localTimezoneChanged + * + * @constructor + * @extends carto.dataview.Base + * @memberof carto.dataview + * @api + * @example + * // We have a tweets dataset and we want to show a "per hour histogram" with the data. + * var timeSeries = new carto.dataview.TimeSeries(source0, 'last_review', { + * offset: 0, + * aggregation: 'hour' + * }); + * @example + * // You can listen to multiple events emmited by the time-series-dataview. + * // Data and status are fired by all dataviews. + * timeSeries.on('dataChanged', newData => { }); + * timeSeries.on('statusChanged', (newData, error) => { }); + * timeSeries.on('error', cartoError => { }); + */ +function TimeSeries (source, column, options) { + this._initialize(source, column, options); + this._aggregation = this._options.aggregation; + this._offset = _hoursToSeconds(this._options.offset); + this._localTimezone = this._options.useLocalTimezone; +} + +TimeSeries.prototype = Object.create(Base.prototype); + +TimeSeries.prototype.DEFAULTS = { + aggregation: timeAggregation.AUTO, + offset: 0, + useLocalTimezone: false +}; + +/** + * Return the resulting data. + * + * @return {carto.dataview.TimeSeriesData} + * @api + */ +TimeSeries.prototype.getData = function () { + if (this._internalModel) { + return parseTimeSeriesData( + this._internalModel.get('data'), + this._internalModel.get('nulls'), + this._internalModel.get('totalAmount'), + this._internalModel.getCurrentOffset() + ); + } + return null; +}; + +/** + * Set time aggregation. + * + * @param {carto.dataview.timeAggregation} aggregation + * @fires aggregationChanged + * @return {carto.dataview.TimeSeries} this + * @api + */ +TimeSeries.prototype.setAggregation = function (aggregation) { + this._validateAggregation(aggregation); + this._changeProperty('aggregation', aggregation); + return this; +}; + +/** + * Return the current time aggregation. + * + * @return {carto.dataview.timeAggregation} Current time aggregation + * @api + */ +TimeSeries.prototype.getAggregation = function () { + return this._aggregation; +}; + +/** + * Set time offset in hours. + * + * @param {number} offset + * @fires offsetChanged + * @return {carto.dataview.TimeSeries} this + * @api + */ +TimeSeries.prototype.setOffset = function (offset) { + this._validateOffset(offset); + this._changeProperty('offset', _hoursToSeconds(offset)); + return this; +}; + +/** + * Return the current time offset in hours. + * + * @return {number} Current time offset + * @api + */ +TimeSeries.prototype.getOffset = function () { + return _secondsToHours(this._offset); +}; + +/** + * Set the local timezone flag. If enabled, the time offset is overriden by the user's local timezone. + * + * @param {boolean} localTimezone + * @fires localTimezoneChanged + * @return {carto.dataview.TimeSeries} this + * @api + */ +TimeSeries.prototype.useLocalTimezone = function (enable) { + this._validateLocalTimezone(enable); + this._changeProperty('localTimezone', enable); + return this; +}; + +/** + * Return the current local timezone flag. + * + * @return {boolean} Current local timezone flag + * @api + */ +TimeSeries.prototype.isUsingLocalTimezone = function () { + return this._localTimezone; +}; + +TimeSeries.prototype._checkOptions = function (options) { + if (_.isUndefined(options)) { + throw this._getValidationError('timeSeriesOptionsRequired'); + } + this._validateAggregation(options.aggregation); + this._validateOffset(options.offset); + this._validateLocalTimezone(options.useLocalTimezone); +}; + +TimeSeries.prototype._validateAggregation = function (aggregation) { + if (!isValidTimeAggregation(aggregation)) { + throw this._getValidationError('timeSeriesInvalidAggregation'); + } +}; + +TimeSeries.prototype._validateOffset = function (offset) { + if (!_.isFinite(offset) || Math.floor(offset) !== offset || offset < -12 || offset > 14) { + throw this._getValidationError('timeSeriesInvalidOffset'); + } +}; + +TimeSeries.prototype._validateLocalTimezone = function (localTimezone) { + if (!_.isBoolean(localTimezone)) { + throw this._getValidationError('timeSeriesInvalidUselocaltimezone'); + } +}; + +TimeSeries.prototype._createInternalModel = function (engine) { + this._internalModel = new HistogramDataviewModel({ + source: this._source.$getInternalModel(), + column: this._column, + aggregation: this._aggregation, + offset: this._offset, + localTimezone: this._localTimezone, + sync_on_bbox_change: !!this._boundingBoxFilter, + sync_on_circle_change: !!this._circleFilter, + sync_on_polygon_change: !!this._polygonFilter, + enabled: this._enabled, + column_type: 'date' + }, { + engine: engine, + bboxFilter: this._boundingBoxFilter && this._boundingBoxFilter.$getInternalModel(), + circleFilter: this._circleFilter && this._circleFilter.$getInternalModel(), + polygonFilter: this._polygonFilter && this._polygonFilter.$getInternalModel() + }); +}; + +// Utility functions + +function _hoursToSeconds (hours) { + return hours * 3600; +} + +function _secondsToHours (seconds) { + return seconds / 3600; +} +module.exports = TimeSeries; + +/** + * Fired when aggregation has changed. Handler gets a parameter with the new aggregation. + * + * @event aggregationChanged + * @type {string} + * @api + */ + +/** + * Fired when localTimezone has changed. Handler gets a parameter with the new timezone. + * + * @event localTimezoneChanged + * @type {boolean} + * @api + */ + +/** + * Fired when offset has changed. Handler gets a parameter with the new offset. + * + * @event offsetChanged + * @type {string} + * @api + */ diff --git a/src/api/v4/dataview/time-series/parse-data.js b/src/api/v4/dataview/time-series/parse-data.js new file mode 100644 index 0000000..81f1c85 --- /dev/null +++ b/src/api/v4/dataview/time-series/parse-data.js @@ -0,0 +1,73 @@ +var _ = require('underscore'); + +function secondsToHours (seconds) { + return seconds / 3600; +} + +/** + * Transform the data obtained from an internal timeseries dataview into a public object. + * + * @param {object[]} data - The raw time series data + * @param {number} nulls - Number of data with a null + * @param {number} totalAmount - Total number of data in the histogram + * + * @return {TimeSeriesData} - The parsed and formatted data for the given parameters + */ +function parseTimeSeriesData (data, nulls, totalAmount, offset) { + if (!data) { + return null; + } + var compactData = _.compact(data); + var maxBin = _.max(compactData, function (bin) { return bin.freq || 0; }); + var maxFreq = _.isFinite(maxBin.freq) && maxBin.freq !== 0 + ? maxBin.freq + : null; + + /** + * @description + * Object containing time series data. + * + * @typedef {object} carto.dataview.TimeSeriesData + * @property {number} nulls - The number of items with null value + * @property {number} totalAmount - The number of elements returned + * @property {number} offset - The time offset in hours. Needed to format UTC timestamps into the proper timezone format + * @property {carto.dataview.TimeSeriesBinItem[]} bins - Array containing the {@link carto.dataview.TimeSeriesBinItem|data bins} for the time series + * @api + */ + return { + bins: _createBins(compactData, maxFreq), + nulls: nulls || 0, + offset: secondsToHours(offset), + totalAmount: totalAmount + }; +} + +/** + * Transform the time series raw data into {@link carto.dataview.TimeSeriesBinItem}. + */ +function _createBins (data, maxFreq) { + return data.map(function (bin) { + /** + * @typedef {object} carto.dataview.TimeSeriesBinItem + * @property {number} index - Number indicating the bin order + * @property {number} start - Starting UTC timestamp of the bin + * @property {number} end - End UTC timestamp of the bin + * @property {number} min - Minimum UTC timestamp present in the bin. Only appears if freq > 0 + * @property {number} max - Maximum UTC timestamp present in the bin. Only appears if freq > 0 + * @property {number} freq - Numbers of elements present in the bin + * @property {number} normalized - Normalized frequency with respect to the whole dataset + * @api + */ + return { + index: bin.bin, + start: bin.start, + end: bin.end, + min: bin.min, + max: bin.max, + freq: bin.freq, + normalized: _.isFinite(bin.freq) && maxFreq > 0 ? bin.freq / maxFreq : 0 + }; + }); +} + +module.exports = parseTimeSeriesData; diff --git a/src/api/v4/error-handling/carto-error-extender.js b/src/api/v4/error-handling/carto-error-extender.js new file mode 100644 index 0000000..fc6cf52 --- /dev/null +++ b/src/api/v4/error-handling/carto-error-extender.js @@ -0,0 +1,92 @@ +var _ = require('underscore'); +var ERROR_LIST = require('./error-list'); + +/** + * Returns two parameters to enrich a CartoError. + * - friendlyMessage: A easy to understand error description. + * - errorCode: Am unique error code + * + * @param {CartoError} cartoError + * + * @returns {object} - An object containing a friendly message and a errorCode + */ +function getExtraFields (cartoError) { + var errorlist = _getErrorList(cartoError); + var listedError = _getListedError(cartoError, errorlist); + + return { + friendlyMessage: listedError.friendlyMessage, + errorCode: listedError.errorCode + }; +} + +/** + * + * @param {CartoError} cartoError + */ +function _getErrorList (cartoError) { + return ERROR_LIST[cartoError.origin] && ERROR_LIST[cartoError.origin][cartoError.type]; +} + +/** + * Get the listed error from a cartoError, if no listedError is found return a generic + * unknown error. + * @param {CartoError} cartoError + */ +function _getListedError (cartoError, errorList) { + var errorListkeys = _.keys(errorList); + var key; + for (var i = 0; i < errorListkeys.length; i++) { + key = errorListkeys[i]; + if (!(errorList[key].messageRegex instanceof RegExp)) { + throw new Error('MessageRegex on ' + key + ' is not a RegExp.'); + } + if (errorList[key].messageRegex.test(cartoError.message)) { + return { + friendlyMessage: _replaceRegex(cartoError, errorList[key]), + errorCode: _buildErrorCode(cartoError, key) + }; + } + } + + // When cartoError not found return generic values + return { + friendlyMessage: cartoError.message || '', + errorCode: _buildErrorCode(cartoError, 'unknown-error') + }; +} + +/** + * Replace $0 and $1 with the proper paramter in the listedError regex to build a friendly message + */ +function _replaceRegex (cartoError, listedError) { + if (!listedError.friendlyMessage) { + return cartoError.message; + } + var match = cartoError.message && cartoError.message.match(listedError.messageRegex); + if (match && match.length > 1) { + var replaced = listedError.friendlyMessage.replace('$0', match[1]); + if (match.length > 2) { + replaced = replaced.replace('$1', match[2]); + } + return replaced; + } + return listedError.friendlyMessage; +} + +/** + * Generate an unique string that represents a cartoError + * @param {cartoError} cartoError + * @param {string} key + */ +function _buildErrorCode (cartoError, key) { + var fragments = []; + fragments.push(cartoError && cartoError.origin); + fragments.push(cartoError && cartoError.type); + fragments.push(key); + fragments = _.compact(fragments); + + return fragments.join(':'); +} + +module.exports = { getExtraFields: getExtraFields }; diff --git a/src/api/v4/error-handling/carto-error.js b/src/api/v4/error-handling/carto-error.js new file mode 100644 index 0000000..9e7082c --- /dev/null +++ b/src/api/v4/error-handling/carto-error.js @@ -0,0 +1,137 @@ +var errorExtender = require('./carto-error-extender'); +var errorTracker = require('./error-tracker'); + +var UNEXPECTED_ERROR = 'unexpected error'; +var GENERIC_ORIGIN = 'generic'; + +/** + * Build a cartoError from a generic error. + * @constructor + * + * @return {CartoError} A well formed object representing the error. + */ +function CartoError (error, opts) { + opts = opts || {}; + var cartoError = Object.create(Error.prototype); + cartoError.message = (error && error.message) || UNEXPECTED_ERROR; + cartoError.origin = (error && error.origin) || GENERIC_ORIGIN; + cartoError.type = (error && error.type) || ''; + + if (_isWindshaftError(error)) { + cartoError = _transformWindshaftError(error, opts.layers, opts.analysis); + } + + if (_isAjaxError(error)) { + cartoError = _transformAjaxError(error); + } + + // Add extra fields + var extraFields = errorExtender.getExtraFields(cartoError); + cartoError.message = extraFields.friendlyMessage; + cartoError.errorCode = extraFields.errorCode; + + // Final properties + cartoError.name = 'CartoError'; + cartoError.stack = (new Error()).stack; + cartoError.originalError = error; + + errorTracker.track(cartoError); + + return cartoError; +} + +// Windshaft should have been parsed already +function _isWindshaftError (error) { + return error && error.origin === 'windshaft'; +} + +function _isAjaxError (error) { + return error && error.responseText; +} + +function _transformWindshaftError (error, layers, analysis) { + var cartoError = Object.create(Error.prototype); + cartoError.message = error.message; + cartoError.origin = error.origin; + cartoError.type = error.type; + + if (error.type === 'layer' && layers) { + cartoError.layer = layers.findById(error.layerId); + } + if (error.type === 'analysis') { + if (analysis) { + cartoError.source = analysis; + cartoError.sourceId = analysis.getId && analysis.getId(); + } + if (error.analysisId) { + cartoError.sourceId = error.analysisId; + } + } + + return cartoError; +} + +function _transformAjaxError (error) { + var cartoError = Object.create(Error.prototype); + cartoError.message = _handleAjaxResponse(error); + cartoError.origin = 'ajax'; + cartoError.type = error.statusText; + + return cartoError; +} + +function _handleAjaxResponse (error) { + var errorMessage = ''; + + try { + var parsedError = JSON.parse(error.responseText); + errorMessage = parsedError.errors[0]; + } catch (exc) { + // Swallow parse error + } + return errorMessage || UNEXPECTED_ERROR; +} + +module.exports = CartoError; + +/** + * Represents an error in the carto library. + * + * Some actions like adding a layer to a map are asynchronous and require a server round trip. + * If some error happens during this communnication with the server, an error with a `CartoError` object + * will be fired. + * + * CartoErrors can be obtained by listening to the client 'error' `client.on('error', callback);`, + * through any async action or by listening to 'error' events on particular objects (eg: dataviews). + * + * Promises are also rejected with a CartoError. + * @example + * // Listen when a layer has been added or there has been an error. + * client.addLayer(layerWithErrors) + * .then(()=> console.log('Layer added succesfully')) + * .catch(cartoError => console.error(cartoError.message)) + * @example + * // Events also will be registered here when the map changes. + * client.on('success', function () { + * console.log('Client reloaded'); + * }); + * + * client.on('error', function (clientError) { + * console.error(clientError.message); + * }); + * @example + * // Listen when there is an error in a dataview + * dataview.on('error', function (error) { + * console.error(error.message); + * }); + * + * @typedef {object} CartoError + * @property {string} message - A short error description + * @property {string} name - The name of the error "CartoError" + * @property {string} origin - Where the error was originated: 'windshaft' | 'ajax' | 'validation' + * @property {object} originalError - An object containing the internal/original error + * @property {object} stack - Error stack trace + * @property {string} type - Error type + * @property {string} sourceId - Available if the error is related to a source object. Indicates the ID of the source that has a problem. + * @api + */ diff --git a/src/api/v4/error-handling/carto-validation-error.js b/src/api/v4/error-handling/carto-validation-error.js new file mode 100644 index 0000000..bc5fc24 --- /dev/null +++ b/src/api/v4/error-handling/carto-validation-error.js @@ -0,0 +1,17 @@ +var CartoError = require('./carto-error'); + +/** + * Utility to build a cartoError related to validation errors. + * @constructor + * + * @return {CartoError} A well formed object representing the error. + */ +function CartoValidationError (type, message, opts) { + return new CartoError({ + origin: 'validation', + type: type, + message: message + }, opts); +} + +module.exports = CartoValidationError; diff --git a/src/api/v4/error-handling/error-list/ajax-errors.js b/src/api/v4/error-handling/error-list/ajax-errors.js new file mode 100644 index 0000000..f4d6253 --- /dev/null +++ b/src/api/v4/error-handling/error-list/ajax-errors.js @@ -0,0 +1,3 @@ +module.exports = { + +}; diff --git a/src/api/v4/error-handling/error-list/index.js b/src/api/v4/error-handling/error-list/index.js new file mode 100644 index 0000000..84f16cf --- /dev/null +++ b/src/api/v4/error-handling/error-list/index.js @@ -0,0 +1,9 @@ +var windshaft = require('./windshaft-errors'); +var ajax = require('./ajax-errors'); +var validation = require('./validation-errors'); + +module.exports = { + ajax: ajax, + windshaft: windshaft, + validation: validation +}; diff --git a/src/api/v4/error-handling/error-list/validation-errors.js b/src/api/v4/error-handling/error-list/validation-errors.js new file mode 100644 index 0000000..8a25c0b --- /dev/null +++ b/src/api/v4/error-handling/error-list/validation-errors.js @@ -0,0 +1,272 @@ +module.exports = { + layer: { + 'non-valid-source': { + messageRegex: /nonValidSource/, + friendlyMessage: 'The given object is not a valid source. See "carto.source.Base".' + }, + 'non-valid-style': { + messageRegex: /nonValidStyle/, + friendlyMessage: 'The given object is not a valid style. See "carto.style.Base".' + }, + 'non-valid-columns': { + messageRegex: /nonValidColumns/, + friendlyMessage: 'The given object is not a valid array of string columns.' + }, + 'source-with-different-client': { + messageRegex: /differentSourceClient/, + friendlyMessage: "A layer can't have a source which belongs to a different client." + }, + 'style-with-different-client': { + messageRegex: /differentStyleClient/, + friendlyMessage: "A layer can't have a style which belongs to a different client." + }, + 'wrong-interactivity-columns': { + messageRegex: /wrongInteractivityColumns\[(.+)\]#(.+)$/, + friendlyMessage: 'Columns [$0] set on `$1` do not match the columns set in aggregation options.' + } + }, + source: { + 'query-required': { + messageRegex: /requiredQuery/, + friendlyMessage: 'SQL Source must have a SQL query.' + }, + 'query-string': { + messageRegex: /requiredString/, + friendlyMessage: 'SQL Query must be a string.' + }, + 'no-dataset-name': { + messageRegex: /noDatasetName/, + friendlyMessage: 'Table name is required.' + }, + 'dataset-string': { + messageRegex: /requiredDatasetString$/, + friendlyMessage: 'Table name must be a string.' + }, + 'dataset-required': { + messageRegex: /requiredDataset$/, + friendlyMessage: 'Table name must be not empty.' + } + }, + style: { + 'required-css': { + messageRegex: /requiredCSS$/, + friendlyMessage: 'CartoCSS is required.' + }, + 'css-string': { + messageRegex: /requiredCSSString$/, + friendlyMessage: 'CartoCSS must be a string.' + } + }, + client: { + 'bad-layer-type': { + messageRegex: /badLayerType/, + friendlyMessage: 'The given object is not a layer.' + }, + 'index-number': { + messageRegex: /indexNumber/, + friendlyMessage: 'index property must be a number.' + }, + 'index-out-of-range': { + messageRegex: /indexOutOfRange/, + friendlyMessage: 'index is out of range.' + }, + 'api-key-required': { + messageRegex: /apiKeyRequired/, + friendlyMessage: 'apiKey property is required.' + }, + 'api-key-string': { + messageRegex: /apiKeyString/, + friendlyMessage: 'apiKey property must be a string.' + }, + 'username-required': { + messageRegex: /usernameRequired/, + friendlyMessage: 'username property is required.' + }, + 'username-string': { + messageRegex: /usernameString/, + friendlyMessage: 'username property must be a string.' + }, + 'non-valid-server-url': { + messageRegex: /nonValidServerURL/, + friendlyMessage: 'serverUrl is not a valid URL.' + }, + 'non-matching-server-url': { + messageRegex: /serverURLDoesntMatchUsername/, + friendlyMessage: "serverUrl doesn't match the username." + }, + 'duplicated-layer-id': { + messageRegex: /duplicatedLayerId/, + friendlyMessage: 'A layer with the same ID already exists in the client.' + } + }, + dataview: { + 'source-required': { + messageRegex: /sourceRequired/, + friendlyMessage: 'Source property is required.' + }, + 'column-required': { + messageRegex: /columnRequired/, + friendlyMessage: 'Column property is required.' + }, + 'column-string': { + messageRegex: /columnString/, + friendlyMessage: 'Column property must be a string.' + }, + 'empty-column': { + messageRegex: /emptyColumn/, + friendlyMessage: 'Column property must be not empty.' + }, + 'filter-required': { + messageRegex: /filterRequired/, + friendlyMessage: 'Filter property is required.' + }, + 'time-series-options-required': { + messageRegex: /timeSeriesOptionsRequired/, + friendlyMessage: 'Options object to create a time series dataview is required.' + }, + 'time-series-invalid-aggregation': { + messageRegex: /timeSeriesInvalidAggregation/, + friendlyMessage: 'Time aggregation must be a valid value. Use carto.dataview.timeAggregation.' + }, + 'time-series-invalid-offset': { + messageRegex: /timeSeriesInvalidOffset/, + friendlyMessage: 'Offset must an integer value between -12 and 14.' + }, + 'time-series-invalid-uselocaltimezone': { + messageRegex: /timeSeriesInvalidUselocaltimezone/, + friendlyMessage: 'useLocalTimezone must be a boolean value.' + }, + 'histogram-options-required': { + messageRegex: /histogramOptionsRequired/, + friendlyMessage: 'Options object to create a histogram dataview is required.' + }, + 'histogram-invalid-bins': { + messageRegex: /histogramInvalidBins/, + friendlyMessage: 'Bins must be a positive integer value.' + }, + 'histogram-invalid-start-end': { + messageRegex: /histogramInvalidStartEnd/, + friendlyMessage: 'Both start and end values must be a number or null.' + }, + 'formula-options-required': { + messageRegex: /formulaOptionsRequired/, + friendlyMessage: 'Formula dataview options are not defined.' + }, + 'formula-invalid-operation': { + messageRegex: /formulaInvalidOperation/, + friendlyMessage: 'Operation for formula dataview is not valid. Use carto.operation' + }, + 'category-options-required': { + messageRegex: /categoryOptionsRequired/, + friendlyMessage: 'Category dataview options are not defined.' + }, + 'category-limit-required': { + messageRegex: /categoryLimitRequired/, + friendlyMessage: 'Limit for category dataview is required.' + }, + 'category-limit-number': { + messageRegex: /categoryLimitNumber/, + friendlyMessage: 'Limit for category dataview must be a number.' + }, + 'category-limit-positive': { + messageRegex: /categoryLimitPositive/, + friendlyMessage: 'Limit for category dataview must be greater than 0.' + }, + 'category-invalid-operation': { + messageRegex: /categoryInvalidOperation/, + friendlyMessage: 'Operation for category dataview is not valid. Use carto.operation' + }, + 'category-operation-required': { + messageRegex: /categoryOperationRequired/, + friendlyMessage: 'Operation column for category dataview is required.' + }, + 'category-operation-string': { + messageRegex: /categoryOperationString/, + friendlyMessage: 'Operation column for category dataview must be a string.' + }, + 'category-operation-empty': { + messageRegex: /categoryOperationEmpty/, + friendlyMessage: 'Operation column for category dataview must be not empty.' + } + }, + filter: { + 'invalid-bounds-object': { + messageRegex: /invalidBoundsObject/, + friendlyMessage: 'Bounds object is not valid. Use a carto.filter.Bounds object' + }, + 'invalid-circle-object': { + messageRegex: /invalidCircleObject/, + friendlyMessage: 'Circle object is not valid. Use a carto.filter.CircleData object' + }, + 'invalid-polygon-object': { + messageRegex: /invalidPolygonObject/, + friendlyMessage: 'Polygon object is not valid. Use a carto.filter.PolygonData object' + }, + 'column-required': { + messageRegex: /columnRequired/, + friendlyMessage: 'Column property is required.' + }, + 'column-string': { + messageRegex: /columnString/, + friendlyMessage: 'Column property must be a string.' + }, + 'empty-column': { + messageRegex: /emptyColumn/, + friendlyMessage: 'Column property must be not empty.' + }, + 'invalid-filter': { + messageRegex: /invalidFilter(.+)/, + friendlyMessage: "'$0' is not a valid filter. Please check documentation." + }, + 'invalid-option': { + messageRegex: /invalidOption(.+)/, + friendlyMessage: "'$0' is not a valid option for this filter." + }, + 'wrong-filter-type': { + messageRegex: /wrongFilterType/, + friendlyMessage: 'Filters need to extend from carto.filter.SQLBase. Please use carto.filter.Category or carto.filter.Range.' + }, + 'invalid-parameter-type': { + messageRegex: /invalidParameterType(.+)/, + friendlyMessage: "Invalid parameter type for '$0'. Please check filters documentation." + } + }, + aggregation: { + 'threshold-required': { + messageRegex: /thresholdRequired/, + friendlyMessage: 'Aggregation threshold is required.' + }, + 'invalid-threshold': { + messageRegex: /invalidThreshold/, + friendlyMessage: 'Aggregation threshold must be an integer value greater than 0.' + }, + 'resolution-required': { + messageRegex: /resolutionRequired/, + friendlyMessage: 'Aggregation resolution is required.' + }, + 'invalid-resolution': { + messageRegex: /invalidResolution/, + friendlyMessage: 'Aggregation resolution must be 0.5, 1 or powers of 2 up to 256 (2, 4, 8, 16, 32, 64, 128, 256).' + }, + 'invalid-placement': { + messageRegex: /invalidPlacement/, + friendlyMessage: 'Aggregation placement is not valid. Must be one of these values: `point-sample`, `point-grid`, `centroid`' + }, + 'column-function-required': { + messageRegex: /columnFunctionRequired(.+)$/, + friendlyMessage: "Aggregation function for column '$0' is required." + }, + 'invalid-column-function': { + messageRegex: /invalidColumnFunction(.+)$/, + friendlyMessage: "Aggregation function for column '$0' is not valid. Use carto.aggregation.function" + }, + 'column-aggregated-column-required': { + messageRegex: /columnAggregatedColumnRequired(.+)$/, + friendlyMessage: "Column to be aggregated to '$0' is required." + }, + 'invalid-column-aggregated-column': { + messageRegex: /invalidColumnAggregatedColumn(.+)$/, + friendlyMessage: "Column to be aggregated to '$0' must be a string." + } + } +}; diff --git a/src/api/v4/error-handling/error-list/windshaft-errors.js b/src/api/v4/error-handling/error-list/windshaft-errors.js new file mode 100644 index 0000000..5e992a9 --- /dev/null +++ b/src/api/v4/error-handling/error-list/windshaft-errors.js @@ -0,0 +1,72 @@ +module.exports = { + analysis: { + 'sql-syntax-error': { + messageRegex: /^syntax error/ + }, + 'invalid-dataset': { + messageRegex: /relation (.+) does not exist/, + friendlyMessage: 'Invalid dataset name used. Dataset $0 does not exist.' + }, + 'column-does-not-exist': { + messageRegex: /column (.+) does not exist/, + friendlyMessage: 'Invalid column name. Column $0 does not exist.' + }, + 'analysis-requires-authentication': { + messageRegex: /^Analysis requires authentication with API key/ + } + }, + generic: { + }, + limit: { + 'over-platform-limits': { + messageRegex: /^You are over platform's limits/ + }, + 'generic-limit-error': { + messageRegex: /.*/, + friendlyMessage: 'The server is taking too long to respond, due to poor conectivity or a temporary error with our servers. Please try again soon.' + } + }, + tile: { + 'generic-tile-error': { + messageRegex: /.*/, + friendlyMessage: 'Some tiles might not be rendering correctly.' + } + }, + layer: { + 'column-does-not-exist': { + messageRegex: /column (.+) does not exist/, + friendlyMessage: 'Invalid column name. Column $0 does not exist.' + }, + 'unrecognized-rule': { + messageRegex: /Unrecognized rule: (.+)/, + friendlyMessage: 'Unrecognized rule "$0"' + }, + 'generic-layer-error': { + messageRegex: /.*/ + } + }, + dataview: { + 'formula-does-not-support-operation': { + messageRegex: /Formula does not support (.+) operation/ + }, + 'column-does-not-exist': { + messageRegex: /column (.+) does not exist/ + }, + 'permission-denied': { + messageRegex: /permission denied for (.+)/ + }, + 'wrong-type-column-used-in-time-series': { + messageRegex: /function date_part\(unknown, (.+)\) does not exist/, + friendlyMessage: 'Your time series column type is $0. Please use a date type.' + }, + 'invalid-aggregation-value': { + messageRegex: /Invalid aggregation value. Valid ones: auto, minute, hour, day, week, month, quarter, year, decade, century, millennium/ + } + }, + auth: { + 'forbidden': { + messageRegex: /^Forbidden$/, + friendlyMessage: 'Forbidden. API key does not grant access.' + } + } +}; diff --git a/src/api/v4/error-handling/error-tracker.js b/src/api/v4/error-handling/error-tracker.js new file mode 100644 index 0000000..1a65776 --- /dev/null +++ b/src/api/v4/error-handling/error-tracker.js @@ -0,0 +1,15 @@ +/** + * Use {@link http://docs.trackjs.com/tracker/top-level-api} for error logging. + */ +function track (error) { + if (window.trackJs) { + try { + var message = error ? error.message + ' - code: ' + error.errorCode : JSON.stringify(error); + window.trackJs.track(new Error(message)); + } catch (exc) { + // Swallow + } + } +} + +module.exports = {track: track}; diff --git a/src/api/v4/events.js b/src/api/v4/events.js new file mode 100644 index 0000000..e610486 --- /dev/null +++ b/src/api/v4/events.js @@ -0,0 +1,19 @@ +/** + * Fired when something went wrong on the server side. + * + * @event error + * @type {CartoError} + * @api + */ + +/** + * Fired when a request to the server completed successfully. + * + * @event success + * @api + */ + +module.exports = { + SUCCESS: 'success', + ERROR: 'error' +}; diff --git a/src/api/v4/filter/and.js b/src/api/v4/filter/and.js new file mode 100644 index 0000000..a995030 --- /dev/null +++ b/src/api/v4/filter/and.js @@ -0,0 +1,34 @@ +const FiltersCollection = require('./filters-collection'); + +/** + * When including this filter into a {@link carto.source.SQL} or a {@link carto.source.Dataset}, the rows will be filtered by the conditions included within filters. + * + * This filter will group as many filters as you want and it will add them to the query returning the rows that match ALL the filters to render the visualization. + * + * You can add or remove filters by invoking `.addFilter()` and `.removeFilter()`. + * + * @example + * // Create a filter by room type, showing only private rooms + * const roomTypeFilter = new carto.filter.Category('room_type', { eq: 'Private room' }); + * // Create a filter by price, showing only listings lower than or equal to 50€ + * const priceFilter = new carto.filter.Range('price', { lte: 50 }); + * + * // Combine the filters with an AND condition, returning rows that match both filters + * const filterByRoomTypeAndPrice = new carto.filter.AND([ roomTypeFilter, priceFilter ]); + * + * // Add filters to the existing source + * source.addFilter(filterByRoomTypeAndPrice); + * + * @class AND + * @extends carto.filter.FiltersCollection + * @memberof carto.filter + * @api + */ +class AND extends FiltersCollection { + constructor (filters) { + super(filters); + this.JOIN_OPERATOR = 'AND'; + } +} + +module.exports = AND; diff --git a/src/api/v4/filter/base-sql.js b/src/api/v4/filter/base-sql.js new file mode 100644 index 0000000..7f32991 --- /dev/null +++ b/src/api/v4/filter/base-sql.js @@ -0,0 +1,195 @@ +const _ = require('underscore'); +const Base = require('./base'); +const getObjectValue = require('../../../../src/util/get-object-value'); + +const ALLOWED_OPTIONS = ['includeNull']; +const DEFAULT_JOIN_OPERATOR = 'AND'; + +/** + * SQL Filter + * + * A SQL filter is the base for all the SQL filters such as the Category Filter or the Range filter + * + * @param {string} column - The filtering will be performed against this column + * @param {object} [options={}] + * @param {boolean} [options.includeNull] - Include null rows when returning data + * + * @class SQLBase + * @extends carto.filter.Base + * @memberof carto.filter + */ +class SQLBase extends Base { + constructor (column, options = {}) { + super(); + + this._checkColumn(column); + this._checkOptions(options); + + this._column = column; + this._filters = {}; + this._options = options; + } + + /** + * Set any of the filter conditions, overwriting the previous one. + * @param {string} filterType - The filter type that you want to set + * @param {string} filterValue - The value of the filter + */ + set (filterType, filterValue) { + if (!filterType || !filterValue || !_.isString(filterType)) { + return; + } + + const newFilter = { [filterType]: filterValue }; + + this._checkFilters(newFilter); + this._filters[filterType] = filterValue; + + this.trigger('change:filters', newFilter); + } + + /** + * Set the filter conditions, overriding all the previous ones. + * @param {object} filters - The object containing all the new filters to apply. + */ + setFilters (filters) { + if (!filters || !_.isObject(filters)) { + return; + } + + this._checkFilters(filters); + this._filters = filters; + + this.trigger('change:filters', filters); + } + + /** + * Remove all conditions from current filter + */ + resetFilters () { + this.setFilters({}); + } + + $getSQL () { + const filters = Object.keys(this._filters); + let sql = filters + .map(filterType => this._interpolateFilter(filterType, this._filters[filterType])) + .filter(filter => Boolean(filter)) + .join(` ${DEFAULT_JOIN_OPERATOR} `); + + if (this._options.includeNull) { + this._includeNullInQuery(sql); + } + + if (filters.length > 1) { + return `(${sql})`; + } + + return sql; + } + + _checkColumn (column) { + if (_.isUndefined(column)) { + throw this._getValidationError('columnRequired'); + } + + if (!_.isString(column)) { + throw this._getValidationError('columnString'); + } + + if (_.isEmpty(column)) { + throw this._getValidationError('emptyColumn'); + } + } + + _checkFilters (filters) { + Object.keys(filters).forEach(filter => { + const isFilterValid = _.contains(this.ALLOWED_FILTERS, filter); + + if (!isFilterValid) { + throw this._getValidationError(`invalidFilter${filter}`); + } + + const parameters = this.PARAMETER_SPECIFICATION[filter].parameters; + const haveCorrectType = parameters.every( + parameter => { + const parameterValue = getObjectValue(filters, parameter.name); + return parameter.allowedTypes.some(type => parameterIsOfType(type, parameterValue)); + } + ); + + if (!haveCorrectType) { + throw this._getValidationError(`invalidParameterType${filter}`); + } + }); + } + + _checkOptions (options) { + Object.keys(options).forEach(option => { + const isOptionValid = _.contains(ALLOWED_OPTIONS, option); + + if (!isOptionValid) { + throw this._getValidationError(`invalidOption${option}`); + } + }); + } + + _convertValueToSQLString (filterValue) { + if (_.isDate(filterValue)) { + return `'${filterValue.toISOString()}'`; + } + + if (_.isArray(filterValue)) { + return filterValue + .map(value => this._convertValueToSQLString(value)) + .join(','); + } + + if (_.isObject(filterValue)) { + Object + .keys(filterValue) + .forEach(key => { + if (key === 'query') { + return; + } + + filterValue[key] = this._convertValueToSQLString(filterValue[key]); + }); + + return filterValue; + } + + if (_.isNumber(filterValue)) { + return filterValue; + } + + return `'${normalizeString(filterValue.toString())}'`; + } + + _interpolateFilter (filterType, filterValues) { + const sqlString = _.template(this.SQL_TEMPLATES[filterType]); + const value = this._convertValueToSQLString(filterValues); + + return sqlString({ column: this._column, value }); + } + + _includeNullInQuery (sql) { + const filters = Object.keys(this._filters); + + if (filters.length > 1) { + sql = `(${sql})`; + } + + return `(${sql} OR ${this._column} IS NULL)`; + } +} + +const parameterIsOfType = function (parameterType, parameterValue) { + return _[`is${parameterType}`](parameterValue); +}; + +const normalizeString = function (value) { + return value.replace(/\n/g, '\\n').replace(/\"/g, '\\"').replace(/'/g, "''"); +}; + +module.exports = SQLBase; diff --git a/src/api/v4/filter/base.js b/src/api/v4/filter/base.js new file mode 100644 index 0000000..f430a6a --- /dev/null +++ b/src/api/v4/filter/base.js @@ -0,0 +1,45 @@ +var _ = require('underscore'); +var Backbone = require('backbone'); +var CartoValidationError = require('../error-handling/carto-validation-error'); + +/** + * Base filter object + * + * @constructor + * @abstract + * @memberof carto.filter + * @api + */ +function Base () {} + +_.extend(Base.prototype, Backbone.Events); + +Base.prototype._getValidationError = function (code) { + return new CartoValidationError('filter', code); +}; + +module.exports = Base; + +/** + * Fired when bounds have changed. Handler gets a parameter with the new bounds. + * + * @event boundsChanged + * @type {carto.filter.Bounds} + * @api + */ + +/** + * Fired when circle filter has changed. Handler gets a parameter with the new circle. + * + * @event circleChanged + * @type {carto.filter.CircleData} + * @api + */ + +/** + * Fired when polygon filter has changed. Handler gets a parameter with the new polygon. + * + * @event polygonChanged + * @type {carto.filter.PolygonData} + * @api + */ diff --git a/src/api/v4/filter/bounding-box-gmaps.js b/src/api/v4/filter/bounding-box-gmaps.js new file mode 100644 index 0000000..5e9ac92 --- /dev/null +++ b/src/api/v4/filter/bounding-box-gmaps.js @@ -0,0 +1,72 @@ +/* global google */ + +var Base = require('./base'); +var GoogleMapsBoundingBoxAdapter = require('../../../geo/adapters/gmaps-bounding-box-adapter'); +var BoundingBoxFilterModel = require('../../../windshaft/filters/bounding-box'); +var utils = require('../../../core/util'); +var SpatialFilterTypes = require('./spatial-filter-types'); + +/** + * Bounding box filter for Google Maps maps. + * + * When this filter is included into a dataview only the data inside the {@link https://developers.google.com/maps/documentation/javascript/3.exp/reference#Map|googleMap} + * bounds will be taken into account. + * + * @param {google.maps.map} map - The google map to track the bounds + * + * @fires boundsChanged + * + * @constructor + * @extends carto.filter.Base + * @memberof carto.filter + * @api + * + * @example + * // Create a bonding box attached to a google map. + * const bboxFilter = new carto.filter.BoundingBoxGoogleMaps(googleMap); + * // Add the filter to a dataview. Generating new data when the map bounds are changed. + * dataview.addFilter(bboxFilter); + */ +function BoundingBoxGoogleMaps (map) { + if (!_isGoogleMap(map)) { + throw new Error('Bounding box requires a Google Maps map but got: ' + map); + } + this.type = SpatialFilterTypes.BBOX; + + // Adapt the Google Maps map to offer unique: + // - getBounds() function + // - 'boundsChanged' event + var mapAdapter = new GoogleMapsBoundingBoxAdapter(map); + // Use the adapter for the internal BoundingBoxFilter model + this._internalModel = new BoundingBoxFilterModel(mapAdapter); + this.listenTo(this._internalModel, 'boundsChanged', this._onBoundsChanged); +} + +BoundingBoxGoogleMaps.prototype = Object.create(Base.prototype); + +/** + * Return the current bounds. + * + * @return {carto.filter.Bounds} Current bounds + * @api + */ +BoundingBoxGoogleMaps.prototype.getBounds = function () { + return this._internalModel.getBounds(); +}; + +BoundingBoxGoogleMaps.prototype._onBoundsChanged = function (bounds) { + this.trigger('boundsChanged', bounds); +}; + +BoundingBoxGoogleMaps.prototype.$getInternalModel = function () { + return this._internalModel; +}; + +// Helper to check if an element is a leafletmap object +function _isGoogleMap (element) { + // Check if Google Maps is loaded + utils.isGoogleMapsLoaded(); + return element instanceof google.maps.Map; +} + +module.exports = BoundingBoxGoogleMaps; diff --git a/src/api/v4/filter/bounding-box-leaflet.js b/src/api/v4/filter/bounding-box-leaflet.js new file mode 100644 index 0000000..5edc9f3 --- /dev/null +++ b/src/api/v4/filter/bounding-box-leaflet.js @@ -0,0 +1,71 @@ +/* global L */ +var Base = require('./base'); +var LeafletBoundingBoxAdapter = require('../../../geo/adapters/leaflet-bounding-box-adapter'); +var BoundingBoxFilterModel = require('../../../windshaft/filters/bounding-box'); +var utils = require('../../../core/util'); +var SpatialFilterTypes = require('./spatial-filter-types'); + +/** + * Bounding box filter for Leaflet maps. + * + * When this filter is included into a dataview only the data inside the {@link http://leafletjs.com/reference-1.3.1.html#map|leafletMap} + * bounds will be taken into account. + * + * @param {L.Map} map - The leaflet map view + * + * @fires boundsChanged + * + * @constructor + * @extends carto.filter.Base + * @memberof carto.filter + * @api + * + * @example + * // Create a bonding box attached to a leaflet map. + * const bboxFilter = new carto.filter.BoundingBoxLeaflet(leafletMap); + * // Add the filter to a dataview. Generating new data when the map bounds are changed. + * dataview.addFilter(bboxFilter); + */ +function BoundingBoxLeaflet (map) { + if (!_isLeafletMap(map)) { + throw new Error('Bounding box requires a Leaflet map but got: ' + map); + } + this.type = SpatialFilterTypes.BBOX; + + // Adapt the Leaflet map to offer unique: + // - getBounds() function + // - 'boundsChanged' event + var mapAdapter = new LeafletBoundingBoxAdapter(map); + // Use the adapter for the internal BoundingBoxFilter model + this._internalModel = new BoundingBoxFilterModel(mapAdapter); + this.listenTo(this._internalModel, 'boundsChanged', this._onBoundsChanged); +} + +BoundingBoxLeaflet.prototype = Object.create(Base.prototype); + +/** + * Return the current bounds. + * + * @return {carto.filter.Bounds} Current bounds + * @api + */ +BoundingBoxLeaflet.prototype.getBounds = function () { + return this._internalModel.getBounds(); +}; + +BoundingBoxLeaflet.prototype._onBoundsChanged = function (bounds) { + this.trigger('boundsChanged', bounds); +}; + +BoundingBoxLeaflet.prototype.$getInternalModel = function () { + return this._internalModel; +}; + +// Helper to check if an element is a Leaflet map object +function _isLeafletMap (element) { + // Check if Leaflet is loaded + utils.isLeafletLoaded(); + return element instanceof L.Map; +} + +module.exports = BoundingBoxLeaflet; diff --git a/src/api/v4/filter/bounding-box.js b/src/api/v4/filter/bounding-box.js new file mode 100644 index 0000000..fc36ace --- /dev/null +++ b/src/api/v4/filter/bounding-box.js @@ -0,0 +1,93 @@ +var _ = require('underscore'); +var Base = require('./base'); +var BoundingBoxFilterModel = require('../../../windshaft/filters/bounding-box'); +var CartoValidationError = require('../error-handling/carto-validation-error'); +var SpatialFilterTypes = require('./spatial-filter-types'); + +/** + * Generic bounding box filter. + * + * When this filter is included into a dataview only the data inside a custom bounding box will be taken into account. + * + * You can manually set the bounds via the `.setBounds()` method. + * + * This filter could be useful if you want give the users to ability to select a portion of the map and update the dataviews accordingly. + * + * + * @constructor + * @fires boundsChanged + * @extends carto.filter.Base + * @memberof carto.filter + * @api + * + */ +function BoundingBox () { + this._internalModel = new BoundingBoxFilterModel(); + this.type = SpatialFilterTypes.BBOX; +} + +BoundingBox.prototype = Object.create(Base.prototype); + +/** + * Set the bounds. + * + * @param {carto.filter.Bounds} bounds + * @fires boundsChanged + * @return {carto.filter.BoundingBox} this + * @api + */ +BoundingBox.prototype.setBounds = function (bounds) { + this._checkBounds(bounds); + this._internalModel.setBounds(bounds); + this.trigger('boundsChanged', bounds); + return this; +}; + +/** + * Reset the bounds. + * + * @fires boundsChanged + * @return {carto.filter.BoundingBox} this + * @api + */ +BoundingBox.prototype.resetBounds = function () { + return this.setBounds({ west: 0, south: 0, east: 0, north: 0 }); +}; + +/** + * Return the current bounds. + * + * @return {carto.filter.Bounds} Current bounds + * @api + */ +BoundingBox.prototype.getBounds = function () { + /** + * @typedef {object} carto.filter.Bounds + * @property {number} west - West coordinate + * @property {number} south - South coordinate + * @property {number} east - East coordinate + * @property {number} north - North coordinate + * @api + */ + return this._internalModel.getBounds(); +}; + +BoundingBox.prototype._checkBounds = function (bounds) { + if (_.isUndefined(bounds) || + _.isUndefined(bounds.west) || + _.isUndefined(bounds.south) || + _.isUndefined(bounds.east) || + _.isUndefined(bounds.north) || + !_.isNumber(bounds.west) || + !_.isNumber(bounds.south) || + !_.isNumber(bounds.east) || + !_.isNumber(bounds.north)) { + throw new CartoValidationError('filter', 'invalidBoundsObject'); + } +}; + +BoundingBox.prototype.$getInternalModel = function () { + return this._internalModel; +}; + +module.exports = BoundingBox; diff --git a/src/api/v4/filter/category.js b/src/api/v4/filter/category.js new file mode 100644 index 0000000..d29adeb --- /dev/null +++ b/src/api/v4/filter/category.js @@ -0,0 +1,107 @@ +const SQLBase = require('./base-sql'); + +const CATEGORY_COMPARISON_OPERATORS = { + in: { parameters: [{ name: 'in', allowedTypes: ['Array', 'String', 'Object'] }] }, + notIn: { parameters: [{ name: 'notIn', allowedTypes: ['Array', 'String', 'Object'] }] }, + eq: { parameters: [{ name: 'eq', allowedTypes: ['String', 'Number', 'Date', 'Object'] }] }, + notEq: { parameters: [{ name: 'notEq', allowedTypes: ['String', 'Number', 'Date', 'Object'] }] }, + like: { parameters: [{ name: 'like', allowedTypes: ['String'] }] }, + similarTo: { parameters: [{ name: 'similarTo', allowedTypes: ['String'] }] } +}; + +const ALLOWED_FILTERS = Object.freeze(Object.keys(CATEGORY_COMPARISON_OPERATORS)); + +/** + * When including this filter into a {@link carto.source.SQL} or a {@link carto.source.Dataset}, the rows will be filtered by the conditions included within the filter. + * + * You can filter columns with `in`, `notIn`, `eq`, `notEq`, `like`, `similarTo` filters, and update the conditions with `.set()` or `.setFilters()` method. It will refresh the visualization automatically when any filter is added or modified. + * + * This filter won't include null values within returned rows by default but you can include them by setting `includeNull` option. + * + * @param {string} column - The column which the filter will be performed against + * @param {object} filters - The filters you want to apply to the table rows + * @param {(string[]|object)} filters.in - Return rows whose column value is included within the provided values + * @param {string} filters.in.query - Return rows whose column value is included within query results + * @param {(string[]|object)} filters.notIn - Return rows whose column value is included within the provided values + * @param {string} filters.notIn.query - Return rows whose column value is not included within query results + * @param {(string|number|Date|object)} filters.eq - Return rows whose column value is equal to the provided value + * @param {string} filters.eq.query - Return rows whose column value is equal to the value returned by query + * @param {(string|number|Date|object)} filters.notEq - Return rows whose column value is not equal to the provided value + * @param {string} filters.notEq.query - Return rows whose column value is not equal to the value returned by query + * @param {string} filters.like - Return rows whose column value is like the provided value + * @param {string} filters.similarTo - Return rows whose column value is similar to the provided values + * @param {object} [options] + * @param {boolean} [options.includeNull] - Include null rows when returning data + * + * @example + * // Create a filter by room type, showing only private rooms + * const roomTypeFilter = new carto.filter.Category('room_type', { eq: 'Private Room' }); + * airbnbDataset.addFilter(roomTypeFilter); + * + * @example + * // Create a filter by room type, showing only private rooms and entire apartments + * const roomTypeFilter = new carto.filter.Category('room_type', { in: ['Private Room', 'Entire home/apt'] }); + * airbnbDataset.addFilter(roomTypeFilter); + * + * @example + * // Create a filter by room type, showing results included in subquery + * const roomTypeFilter = new carto.filter.Category('room_type', { in: { query: 'SELECT distinct(type) FROM rooms' } }); + * airbnbDataset.addFilter(roomTypeFilter); + * + * @class Category + * @extends carto.filter.Base + * @memberof carto.filter + * @api + */ +class Category extends SQLBase { + constructor (column, filters = {}, options) { + super(column, options); + + this.SQL_TEMPLATES = this._getSQLTemplates(); + this.ALLOWED_FILTERS = ALLOWED_FILTERS; + this.PARAMETER_SPECIFICATION = CATEGORY_COMPARISON_OPERATORS; + + this._checkFilters(filters); + this._filters = filters; + } + + _getSQLTemplates () { + return { + in: '<% if (value) { %><%= column %> IN (<%= value.query || value %>)<% } else { %>true = false<% } %>', + notIn: '<% if (value) { %><%= column %> NOT IN (<%= value.query || value %>)<% } %>', + eq: '<%= column %> = <%= value.query ? "(" + value.query + ")" : value %>', + notEq: '<%= column %> != <%= value.query ? "(" + value.query + ")" : value %>', + like: '<%= column %> LIKE <%= value %>', + similarTo: '<%= column %> SIMILAR TO <%= value %>' + }; + } + + /** + * Set any of the filter conditions, overwriting the previous one. + * @param {string} filterType - The filter type that you want to set. `in`, `notIn`, `eq`, `notEq`, `like`, `similarTo`. + * @param {string} filterValue - The value of the filter. Check types in {@link carto.filter.Category} + * + * @memberof Category + * @method set + * @api + */ + + /** + * Set filter conditions, overriding all the previous ones. + * @param {object} filters - Object containing all the new filters to apply. Check filter options in {@link carto.filter.Category}. + * + * @memberof Category + * @method setFilters + * @api + */ + + /** + * Remove all conditions from current filter + * + * @memberof Category + * @method resetFilters + * @api + */ +} + +module.exports = Category; diff --git a/src/api/v4/filter/circle.js b/src/api/v4/filter/circle.js new file mode 100644 index 0000000..675cdf9 --- /dev/null +++ b/src/api/v4/filter/circle.js @@ -0,0 +1,90 @@ +var _ = require('underscore'); +var Base = require('./base'); +var CircleFilterModel = require('../../../windshaft/filters/circle'); +var CartoValidationError = require('../error-handling/carto-validation-error'); +var SpatialFilterTypes = require('./spatial-filter-types'); + +/** + * Generic circle filter. + * + * When this filter is included into a dataview only the data inside a custom circle will be taken into account. + * + * You can manually set the circle properties with the `setCircle()`. + * + * This filter could be useful if you want give the users the ability to select a buffer around a point of interest in the map and update the dataviews accordingly. + * + * + * @constructor + * @fires circleChanged + * @extends carto.filter.Base + * @memberof carto.filter + * @api + * + */ +function Circle () { + this._internalModel = new CircleFilterModel(); + this.type = SpatialFilterTypes.CIRCLE; +} + +Circle.prototype = Object.create(Base.prototype); + +/** + * Set the circle. + * + * @param {carto.filter.CircleData} circle + * @fires circleChanged + * @return {carto.filter.Circle} this + * @api + */ +Circle.prototype.setCircle = function (circle) { + this._checkCircle(circle); + this._internalModel.setCircle(circle); + this.trigger('circleChanged', circle); + return this; +}; + +/** + * Reset the circle. + * + * @fires circleChanged + * @return {carto.filter.Circle} this + * @api + */ +Circle.prototype.resetCircle = function () { + return this.setCircle({ lat: 0, lng: 0, radius: 0 }); +}; + +/** + * Return the current circle data + * + * @return {carto.filter.CircleData} Current circle data + * @api + */ +Circle.prototype.getCircle = function () { + /** + * @typedef {object} carto.filter.CircleData + * @property {number} lat - Center Latitude WGS84 + * @property {number} lng - Center Longitude WGS84 + * @property {number} radius - Radius in meters + * @api + */ + return this._internalModel.getCircle(); +}; + +Circle.prototype._checkCircle = function (circle) { + if (_.isUndefined(circle) || + _.isUndefined(circle.lat) || + _.isUndefined(circle.lng) || + _.isUndefined(circle.radius) || + !_.isNumber(circle.lat) || + !_.isNumber(circle.lng) || + !_.isNumber(circle.radius)) { + throw new CartoValidationError('filter', 'invalidCircleObject'); + } +}; + +Circle.prototype.$getInternalModel = function () { + return this._internalModel; +}; + +module.exports = Circle; diff --git a/src/api/v4/filter/filters-collection.js b/src/api/v4/filter/filters-collection.js new file mode 100644 index 0000000..b651e94 --- /dev/null +++ b/src/api/v4/filter/filters-collection.js @@ -0,0 +1,112 @@ +const _ = require('underscore'); +const Base = require('./base'); +const SQLBase = require('./base-sql'); + +const DEFAULT_JOIN_OPERATOR = 'AND'; + +/** + * Base class for AND and OR filters. + * + * Filters Collection is a way to group a set of filters in order to create composed filters, allowing the user to change the operator that joins the filters. + * + * **This object should not be used directly.** + * + * @class FiltersCollection + * @abstract + * @extends carto.filter.Base + * @memberof carto.filter + * @api + */ +class FiltersCollection extends Base { + constructor (filters) { + super(); + this._initialize(filters); + } + + _initialize (filters) { + this._filters = []; + + if (filters && filters.length) { + filters.map(filter => this.addFilter(filter)); + } + } + + /** + * Add a new filter to collection + * + * @param {(carto.filter.Range|carto.filter.Category|carto.filter.AND|carto.filter.OR)} filter + * @memberof FiltersCollection + * @api + */ + addFilter (filter) { + if (!(filter instanceof SQLBase) && !(filter instanceof FiltersCollection)) { + throw this._getValidationError('wrongFilterType'); + } + + if (_.contains(this._filters, filter)) return; + + this.listenTo(filter, 'change:filters', filters => this._triggerFilterChange(filters)); + this._filters.push(filter); + this._triggerFilterChange(); + } + + /** + * Remove an existing filter from collection + * + * @param {(carto.filter.Range|carto.filter.Category|carto.filter.AND|carto.filter.OR)} filter + * @returns {(carto.filter.Range|carto.filter.Category|carto.filter.AND|carto.filter.OR)} The removed element + * @memberof FiltersCollection + * @api + */ + removeFilter (filter) { + const filterIndex = _.indexOf(this._filters, filter); + if (filterIndex === -1) return; + + const removedElement = this._filters.splice(filterIndex, 1)[0]; + removedElement.off('change:filters', null, this); + + this._triggerFilterChange(); + return removedElement; + } + + /** + * Get the number of added filters + * + * @returns {number} Number of contained filters + * @memberof FiltersCollection + * @api + */ + count () { + return this._filters.length; + } + + /** + * Get added filters + * + * @returns {Array} Added filters + * @memberof FiltersCollection + * @api + */ + getFilters () { + return this._filters; + } + + $getSQL () { + const sqlFilters = this._filters.map(filter => filter.$getSQL()) + .filter(sqlString => Boolean(sqlString)); + + const joinedFilters = sqlFilters.join(` ${this.JOIN_OPERATOR || DEFAULT_JOIN_OPERATOR} `); + + if (sqlFilters.length > 1) { + return `(${joinedFilters})`; + } + + return joinedFilters; + } + + _triggerFilterChange (filters) { + this.trigger('change:filters', filters); + } +} + +module.exports = FiltersCollection; diff --git a/src/api/v4/filter/index.js b/src/api/v4/filter/index.js new file mode 100644 index 0000000..4246901 --- /dev/null +++ b/src/api/v4/filter/index.js @@ -0,0 +1,25 @@ +const BoundingBox = require('./bounding-box'); +const BoundingBoxLeaflet = require('./bounding-box-leaflet'); +const BoundingBoxGoogleMaps = require('./bounding-box-gmaps'); +const Circle = require('./circle'); +const Polygon = require('./polygon'); +const Category = require('./category'); +const Range = require('./range'); +const AND = require('./and'); +const OR = require('./or'); + +/** + * @namespace carto.filter + * @api + */ +module.exports = { + BoundingBox, + BoundingBoxLeaflet, + BoundingBoxGoogleMaps, + Circle, + Polygon, + Category, + Range, + AND, + OR +}; diff --git a/src/api/v4/filter/or.js b/src/api/v4/filter/or.js new file mode 100644 index 0000000..b711989 --- /dev/null +++ b/src/api/v4/filter/or.js @@ -0,0 +1,38 @@ +const FiltersCollection = require('./filters-collection'); + +/** + * When including this filter into a {@link carto.source.SQL} or a {@link carto.source.Dataset}, the rows will be filtered by the conditions included within filters. + * + * This filter will group as many filters as you want and it will add them to the query returning the rows that match ANY of the filters to render the visualization. + * + * You can add or remove filters by invoking `.addFilter()` and `.removeFilter()`. + * + * @example + * // Create a filter by room type, showing only private rooms + * const roomTypeFilter = new carto.filter.Category('room_type', { eq: 'Private room' }); + * // Create a filter by price, showing only listings lower than or equal to 50€ + * const priceFilter = new carto.filter.Range('price', { lte: 50 }); + * + * // Combine the filters with an OR operator, returning rows that match one or the other filter + * const filterByRoomTypeOrPrice = new carto.filter.OR([ roomTypeFilter, priceFilter ]); + * + * // Add filters to the existing source + * source.addFilter(filterByRoomTypeOrPrice); + * + * @class OR + * @extends carto.filter.FiltersCollection + * @memberof carto.filter + * @api + */ +class OR extends FiltersCollection { + /** + * Create a OR group filter + * @param {Array} filters - The filters to apply in the query + */ + constructor (filters) { + super(filters); + this.JOIN_OPERATOR = 'OR'; + } +} + +module.exports = OR; diff --git a/src/api/v4/filter/polygon.js b/src/api/v4/filter/polygon.js new file mode 100644 index 0000000..17bfa8b --- /dev/null +++ b/src/api/v4/filter/polygon.js @@ -0,0 +1,91 @@ +var _ = require('underscore'); +var Base = require('./base'); +var PolygonFilterModel = require('../../../windshaft/filters/polygon'); +var CartoValidationError = require('../error-handling/carto-validation-error'); +var SpatialFilterTypes = require('./spatial-filter-types'); + +/** + * Generic polygon filter. + * + * When this filter is included into a dataview only the data inside a custom polygon will be taken into account. + * + * You can manually set the polygon with the `setPolygon()`. + * + * This filter could be useful if you want give the users the ability to select a custom area in the map and update the dataviews accordingly. + * + * + * @constructor + * @fires polygonChanged + * @extends carto.filter.Base + * @memberof carto.filter + * @api + * + */ +function Polygon () { + this._internalModel = new PolygonFilterModel(); + this.type = SpatialFilterTypes.POLYGON; +} + +Polygon.prototype = Object.create(Base.prototype); + +/** + * Set the polygon. + * + * @param {carto.filter.PolygonData} polygon + * @fires polygonChanged + * @return {carto.filter.Polygon} this + * @api + */ +Polygon.prototype.setPolygon = function (polygon) { + this._checkPolygon(polygon); + this._internalModel.setPolygon(polygon); + this.trigger('polygonChanged', polygon); + return this; +}; + +/** + * Reset the polygon. + * + * @fires polygonChanged + * @return {carto.filter.Polygon} this + * @api + */ +Polygon.prototype.resetPolygon = function () { + return this.setPolygon({ + type: 'Polygon', + coordinates: [] + }); +}; + +/** + * Return the current polygon data + * + * @return {carto.filter.PolygonData} Current polygon data, expressed as a GeoJSON geometry fragment + * @api + */ +Polygon.prototype.getPolygon = function () { + /** + * @typedef {object} carto.filter.PolygonData + * @property {string} type - Geometry type, Just 'Polygon' is valid + * @property {Array.} coordinates - Array of coordinates [lng, lat] as defined in GeoJSON geometries + * @api + */ + return this._internalModel.getPolygon(); +}; + +Polygon.prototype._checkPolygon = function (polygon) { + if (_.isUndefined(polygon) || + _.isUndefined(polygon.type) || + _.isUndefined(polygon.coordinates) || + !_.isString(polygon.type) || + !_.isArray(polygon.coordinates) || + polygon.type !== 'Polygon') { + throw new CartoValidationError('filter', 'invalidPolygonObject'); + } +}; + +Polygon.prototype.$getInternalModel = function () { + return this._internalModel; +}; + +module.exports = Polygon; diff --git a/src/api/v4/filter/range.js b/src/api/v4/filter/range.js new file mode 100644 index 0000000..52932bc --- /dev/null +++ b/src/api/v4/filter/range.js @@ -0,0 +1,140 @@ +const SQLBase = require('./base-sql'); + +const RANGE_COMPARISON_OPERATORS = { + lt: { parameters: [{ name: 'lt', allowedTypes: ['Number', 'Date', 'Object'] }] }, + lte: { parameters: [{ name: 'lte', allowedTypes: ['Number', 'Date', 'Object'] }] }, + gt: { parameters: [{ name: 'gt', allowedTypes: ['Number', 'Date', 'Object'] }] }, + gte: { parameters: [{ name: 'gte', allowedTypes: ['Number', 'Date', 'Object'] }] }, + between: { + parameters: [ + { name: 'between.min', allowedTypes: ['Number', 'Date'] }, + { name: 'between.max', allowedTypes: ['Number', 'Date'] } + ] + }, + notBetween: { + parameters: [ + { name: 'notBetween.min', allowedTypes: ['Number', 'Date'] }, + { name: 'notBetween.max', allowedTypes: ['Number', 'Date'] } + ] + }, + betweenSymmetric: { + parameters: [ + { name: 'betweenSymmetric.min', allowedTypes: ['Number', 'Date'] }, + { name: 'betweenSymmetric.max', allowedTypes: ['Number', 'Date'] } + ] + }, + notBetweenSymmetric: { + parameters: [ + { name: 'notBetweenSymmetric.min', allowedTypes: ['Number', 'Date'] }, + { name: 'notBetweenSymmetric.max', allowedTypes: ['Number', 'Date'] } + ] + } +}; + +const ALLOWED_FILTERS = Object.freeze(Object.keys(RANGE_COMPARISON_OPERATORS)); + +/** + * When including this filter into a {@link carto.source.SQL} or a {@link carto.source.Dataset}, the rows will be filtered by the conditions included within the filter. + * + * You can filter columns with `in`, `notIn`, `eq`, `notEq`, `like`, `similarTo` filters, and update the conditions with `.set()` or `.setFilters()` method. It will refresh the visualization automatically when any filter is added or modified. + * + * This filter won't include null values within returned rows by default but you can include them by setting `includeNull` option. + * + * @param {string} column - The column to filter rows + * @param {object} filters - The filters you want to apply to the column + * @param {(number|Date|object)} filters.lt - Return rows whose column value is less than the provided value + * @param {string} filters.lt.query - Return rows whose column value is less than the value returned by query + * @param {(number|Date|object)} filters.lte - Return rows whose column value is less than or equal to the provided value + * @param {string} filters.lte.query - Return rows whose column value is less than or equal to the value returned by query + * @param {(number|Date|object)} filters.gt - Return rows whose column value is greater than the provided value + * @param {string} filters.gt.query - Return rows whose column value is greater than the value returned by query + * @param {(number|Date|object)} filters.gte - Return rows whose column value is greater than or equal to the provided value + * @param {string} filters.gte.query - Return rows whose column value is greater than or equal to the value returned by query + * @param {(number|Date)} filters.between - Return rows whose column value is between the provided values + * @param {(number|Date)} filters.between.min - Lower value of the comparison range + * @param {(number|Date)} filters.between.max - Upper value of the comparison range + * @param {(number|Date)} filters.notBetween - Return rows whose column value is not between the provided values + * @param {(number|Date)} filters.notBetween.min - Lower value of the comparison range + * @param {(number|Date)} filters.notBetween.max - Upper value of the comparison range + * @param {(number|Date)} filters.betweenSymmetric - Return rows whose column value is between the provided values after sorting them + * @param {(number|Date)} filters.betweenSymmetric.min - Lower value of the comparison range + * @param {(number|Date)} filters.betweenSymmetric.max - Upper value of the comparison range + * @param {(number|Date)} filters.notBetweenSymmetric - Return rows whose column value is not between the provided values after sorting them + * @param {(number|Date)} filters.notBetweenSymmetric.min - Lower value of the comparison range + * @param {(number|Date)} filters.notBetweenSymmetric.max - Upper value of the comparison range + * @param {object} [options] + * @param {boolean} [options.includeNull] - Include null rows when returning data + * + * @example + * // Create a filter by price, showing only listings lower than or equal to 50€, and higher than 100€ + * const priceFilter = new carto.filter.Range('price', { lte: 50, gt: 100 }); + * + * // Add filter to the existing source + * airbnbDataset.addFilter(priceFilter); + * + * @example + * // Create a filter by price, showing only listings greater than or equal to the average price + * const priceFilter = new carto.filter.Range('price', { gte: { query: 'SELECT avg(price) FROM listings' } }); + * + * // Add filter to the existing source + * airbnbDataset.addFilter(priceFilter); + * + * @class Range + * @extends carto.filter.Base + * @memberof carto.filter + * @api + */ +class Range extends SQLBase { + constructor (column, filters = {}, options) { + super(column, options); + + this.SQL_TEMPLATES = this._getSQLTemplates(); + this.ALLOWED_FILTERS = ALLOWED_FILTERS; + this.PARAMETER_SPECIFICATION = RANGE_COMPARISON_OPERATORS; + + this._checkFilters(filters); + this._filters = filters; + } + + _getSQLTemplates () { + return { + lt: '<%= column %> < <%= value.query ? "(" + value.query + ")" : value %>', + lte: '<%= column %> <= <%= value.query ? "(" + value.query + ")" : value %>', + gt: '<%= column %> > <%= value.query ? "(" + value.query + ")" : value %>', + gte: '<%= column %> >= <%= value.query ? "(" + value.query + ")" : value %>', + between: '<%= column %> BETWEEN <%= value.min %> AND <%= value.max %>', + notBetween: '<%= column %> NOT BETWEEN <%= value.min %> AND <%= value.max %>', + betweenSymmetric: '<%= column %> BETWEEN SYMMETRIC <%= value.min %> AND <%= value.max %>', + notBetweenSymmetric: '<%= column %> NOT BETWEEN SYMMETRIC <%= value.min %> AND <%= value.max %>' + }; + } + + /** + * Set any of the filter conditions, overwriting the previous one. + * @param {string} filterType - The filter type that you want to set. `lt`, `lte`, `gt`, `gte`, `between`, `notBetween`, `betweenSymmetric`, `notBetweenSymmetric`. + * @param {string} filterValue - The value of the filter. Check types in {@link carto.filter.Range} + * + * @memberof Range + * @method set + * @api + */ + + /** + * Set filter conditions, overriding all the previous ones. + * @param {object} filters - Object containing all the new filters to apply. Check filter options in {@link carto.filter.Range}. + * + * @memberof Range + * @method setFilters + * @api + */ + + /** + * Remove all conditions from current filter + * + * @memberof Range + * @method resetFilters + * @api + */ +} + +module.exports = Range; diff --git a/src/api/v4/filter/spatial-filter-types.js b/src/api/v4/filter/spatial-filter-types.js new file mode 100644 index 0000000..48cad59 --- /dev/null +++ b/src/api/v4/filter/spatial-filter-types.js @@ -0,0 +1,10 @@ +/** + * Types of spatial filters + */ +var types = { + BBOX: 'bbox', + CIRCLE: 'circle', + POLYGON: 'polygon' +}; + +module.exports = types; diff --git a/src/api/v4/index.js b/src/api/v4/index.js new file mode 100644 index 0000000..a246154 --- /dev/null +++ b/src/api/v4/index.js @@ -0,0 +1,50 @@ +/** + * @api + * @namespace carto + * + * @description + * # CARTO.js + * All the library features are exposed through the `carto` namespace. + * + * + * - **Client** : The api client. + * - **source** : Source description + * - **style** : Style description + * - **layer** : Layer description + * - **dataview** : Dataview description + * - **filter** : Filter description + * - **events** : The events exposed. + * - **operation** : The operations exposed. + */ + +// Add polyfill for `fetch` +require('whatwg-fetch'); +// Add polyfill for `Promise` +var Promise = require('promise-polyfill'); +if (!window.Promise) { + window.Promise = Promise; +} + +var Client = require('./client'); +var source = require('./source'); +var style = require('./style'); +var layer = require('./layer'); +var dataview = require('./dataview'); +var filter = require('./filter'); +var events = require('./events'); +var constants = require('./constants'); + +var carto = { + version: require('../../../package.json').version, + ATTRIBUTION: constants.ATTRIBUTION, + Client: Client, + source: source, + style: style, + layer: layer, + dataview: dataview, + filter: filter, + events: events, + operation: constants.operation +}; + +module.exports = carto; diff --git a/src/api/v4/layer/aggregation.js b/src/api/v4/layer/aggregation.js new file mode 100644 index 0000000..68ef527 --- /dev/null +++ b/src/api/v4/layer/aggregation.js @@ -0,0 +1,178 @@ +var _ = require('underscore'); +var CartoValidationError = require('../error-handling/carto-validation-error'); + +/** + * List of possible aggregation operations. + * See {@link https://carto.com/developers/maps-api/tile-aggregation#columns } for more info. + * @enum {string} carto.layer.Aggregation.operation + * @memberof carto.layer.Aggregation + * @api + */ +var OPERATIONS = { + /** The new point will contain the average value of the or the aggregated ones */ + AVG: 'avg', + /** The new point will contain the sum of the aggregated values */ + SUM: 'sum', + /** The new point will contain the minimal value existing the aggregated features */ + MIN: 'min', + /** The new point will contain the maximun value existing the aggregated features */ + MAX: 'max', + /** The new point will contain the mode of the aggregated values */ + MODE: 'mode' +}; + +/** + * List of possible aggregation feature placements. + * See {@link https://carto.com/developers/maps-api/tile-aggregation#placement } for more info. + * @enum {string} carto.layer.Aggregation.placement + * @memberof carto.layer.Aggregation + * @api + */ +var PLACEMENTS = { + /** The new point will be placed at a random sample of the aggregated points */ + SAMPLE: 'point-sample', + /** The new point will be placed at the center of the aggregation grid cells */ + GRID: 'point-grid', + /** The new point will be placed at averaged coordinated of the grouped points */ + CENTROID: 'centroid' +}; + +var VALID_RESOLUTIONS = [0.5, 1, 2, 4, 8, 16, 32, 64, 128, 256]; + +/** + * An aggregation can be passed to a {@link carto.layer.Layer} to reduce the number of visible points + * increasing the performance. + * + * See {@link https://carto.com/developers/maps-api/guides/tile-aggregation/} for more info. + * + * @param {object} opts + * @param {number} opts.threshold - The minimum number of rows in the dataset for aggregation to be applied + * @param {number} opts.resolution - The cell-size of the spatial aggregation grid [more info]{@link https://carto.com/developers/maps-api/tile-aggregation#resolution} + * @param {string} opts.placement - The kind of [aggregated geometry]{@link https://carto.com/developers/maps-api/tile-aggregation#placement} generated + * @param {object} opts.columns - The new columns are computed by a applying an aggregate function to all the points in each group + * @param {string} opts.columns.aggregatedFunction - The Function used to aggregate the points: avg (average), sum, min (minimum), max (maximum) and mode (the most frequent value in the group) + * @param {string} opts.columns.aggregatedColumn - The name of the original column to be aggregated. + * + * @example + * // Create a layer with aggregated data. + * const aggregationOptions = { + * // CARTO applies aggregation if your dataset has more than threshold rows. In this case, more than 1 row. + * threshold: 1, + * // Defines the cell-size of the aggregation grid. In this case, 1x1 pixel. + * resolution: 1, + * // Where the new point will be placed. In this case, at the center of the grid. + * placement: carto.layer.Aggregation.placement.GRID, + * // Here we define the aggregated columns that we want to obtain. + * columns: { + * // Each property key is the name of the new generated column + * avg_population: { + * // The aggregated column will contain the average of the original data. + * aggregateFunction: carto.layer.Aggregation.operation.AVG, + * // The column to be aggregated + * aggregatedColumn: 'population' + * }, { + * min_population: { + * aggregateFunction: carto.layer.Aggregation.operation.MIN, + * aggregatedColumn: 'population' + * } + * }; + * const aggregation = new Aggregation(options); + * const layer = new carto.layer.Layer(source, style, { aggregation: aggregation }); + * + * @constructor + * @api + * @memberof carto.layer + */ +function Aggregation (opts) { + if (!_.isFinite(opts.threshold)) { + throw _getValidationError('thresholdRequired'); + } + + if (!_.isFinite(opts.threshold) || opts.threshold < 1 || Math.floor(opts.threshold) !== opts.threshold) { + throw _getValidationError('invalidThreshold'); + } + + if (!_.isFinite(opts.resolution)) { + throw _getValidationError('resolutionRequired'); + } + + if (!_.contains(VALID_RESOLUTIONS, opts.resolution)) { + throw _getValidationError('invalidResolution'); + } + + _checkValidPlacement(opts.placement); + + var columns = _checkAndTransformColumns(opts.columns); + + var aggregation = { + threshold: opts.threshold, + resolution: opts.resolution, + placement: opts.placement, + columns: columns + }; + + return _.pick(aggregation, _.identity); // Remove empty values +} + +Aggregation.operation = OPERATIONS; + +Aggregation.placement = PLACEMENTS; + +function _checkColumns (columns) { + Object.keys(columns).forEach(function (key) { + _checkColumn(columns, key); + }); +} + +function _checkColumn (columns, key) { + if (!columns[key].aggregatedColumn) { + throw _getValidationError('columnAggregatedColumnRequired' + key); + } + + if (!_.isString(columns[key].aggregatedColumn)) { + throw _getValidationError('invalidColumnAggregatedColumn' + key); + } + + if (!columns[key].aggregateFunction) { + throw _getValidationError('columnFunctionRequired' + key); + } + + if (!_.contains(_.values(OPERATIONS), columns[key].aggregateFunction)) { + throw _getValidationError('invalidColumnFunction' + key); + } +} + +function _getValidationError (code) { + return new CartoValidationError('aggregation', code); +} + +// Windshaft uses snake_case for column parameters +function _checkAndTransformColumns (columns) { + var returnValue = null; + + if (columns) { + _checkColumns(columns); + + returnValue = {}; + Object.keys(columns).forEach(function (key) { + returnValue[key] = _columnToSnakeCase(columns[key]); + }); + } + return returnValue; +} + +// Windshaft uses snake_case for column parameters +function _columnToSnakeCase (column) { + return { + aggregate_function: column.aggregateFunction, + aggregated_column: column.aggregatedColumn + }; +} + +function _checkValidPlacement (placement) { + if (placement && !_.contains(_.values(PLACEMENTS), placement)) { + throw _getValidationError('invalidPlacement'); + } +} + +module.exports = Aggregation; diff --git a/src/api/v4/layer/base.js b/src/api/v4/layer/base.js new file mode 100644 index 0000000..908f01f --- /dev/null +++ b/src/api/v4/layer/base.js @@ -0,0 +1,51 @@ +var _ = require('underscore'); +var Backbone = require('backbone'); + +/** + * Base layer object. + * + * This object should not be used directly! use {@link carto.layer.Layer} instead. + * + * @constructor + * @abstract + * @fires error + * @memberof carto.layer + * @api + */ +function Base (source, layer, options) { + options = options || {}; + this._id = options.id || Base.$generateId(); +} + +_.extend(Base.prototype, Backbone.Events); + +/** + * Get the unique autogenerated id. + * + * @return {string} Unique autogenerated id + * + */ +Base.prototype.getId = function () { + return this._id; +}; + +/** + * The instance id will be autogenerated by incrementing this variable. + */ +Base.$nextId = 0; + +/** + * Static funciton used internally to autogenerate source ids. + */ +Base.$generateId = function () { + return 'L' + ++Base.$nextId; +}; + +/** + * Return the real CARTO.js model used by the layer. + */ +Base.prototype.$getInternalModel = function () { + return this._internalModel; +}; + +module.exports = Base; diff --git a/src/api/v4/layer/event-types.js b/src/api/v4/layer/event-types.js new file mode 100644 index 0000000..7884744 --- /dev/null +++ b/src/api/v4/layer/event-types.js @@ -0,0 +1,27 @@ +/** + * Events fired by a layer + * + * @enum {string} + * @readonly + * @memberof carto.layer + */ +var events = { + /** + * A feature has been clicked, fired every time the user clicks on a feature. + */ + FEATURE_CLICKED: 'featureClicked', + /** + * The mouse is over a feature, fired every time the user moves over a feature. + */ + FEATURE_OVER: 'featureOver', + /** + * The mouse exits a feature, fired every time the user moves out of a feature. + */ + FEATURE_OUT: 'featureOut', + /** + * There has been an error related to tiles, fired every time the features are not rendered due to an error. + */ + TILE_ERROR: 'featureError' +}; + +module.exports = events; diff --git a/src/api/v4/layer/index.js b/src/api/v4/layer/index.js new file mode 100644 index 0000000..216bce7 --- /dev/null +++ b/src/api/v4/layer/index.js @@ -0,0 +1,18 @@ +var Layer = require('./layer'); +var EventTypes = require('./event-types'); +var Aggregation = require('./aggregation'); + +/** + * @namespace carto.layer + * @api + */ +module.exports = { + Aggregation: Aggregation, + Layer: Layer, + events: EventTypes +}; + +/** + * @namespace carto.layer.metadata + * @api + */ diff --git a/src/api/v4/layer/layer.js b/src/api/v4/layer/layer.js new file mode 100644 index 0000000..53c2a77 --- /dev/null +++ b/src/api/v4/layer/layer.js @@ -0,0 +1,576 @@ +var _ = require('underscore'); +var Base = require('./base'); +var CartoDBLayer = require('../../../geo/map/cartodb-layer'); +var SourceBase = require('../source/base'); +var StyleBase = require('../style/base'); +var CartoError = require('../error-handling/carto-error'); +var CartoValidationError = require('../error-handling/carto-validation-error'); +var EVENTS = require('../events'); +var metadataParser = require('./metadata/parser'); + +/** + * Represents a layer Object. + * + * A layer is the primary way to visualize geospatial data. + * + * To create a layer a {@link carto.source.Base|source} and {@link carto.style.Base|styles} + * are required: + * + * - The {@link carto.source.Base|source} is used to know **what** data will be displayed in the Layer. + * - The {@link carto.style.Base|style} is used to know **how** to draw the data in the Layer. + * + * A layer alone won't do too much. In order to get data from the CARTO server you must add the Layer to a {@link carto.Client|client}. + * + * ``` + * // Create a layer. Remember this won't do anything unless the layer is added to a client. + * const layer = new carto.layer.Layer(source, style); + *``` + * + * @param {carto.source.Base} source - The source where the layer will fetch the data + * @param {carto.style.CartoCSS} style - A CartoCSS object with the layer styling + * @param {object} [options] + * @param {Array} [options.featureClickColumns=[]] - Columns that will be available for `featureClick` events + * @param {boolean} [options.visible=true] - A boolean value indicating the layer's visibility + * @param {Array} [options.featureOverColumns=[]] - Columns that will be available for `featureOver` events + * @param {carto.layer.Aggregation} [options.aggregation={}] - Specify {@link carto.layer.Aggregation|aggregation } options + * @param {string} [options.id] - An unique identifier for the layer + * @fires metadataChanged + * @fires featureClicked + * @fires featureOut + * @fires featureOver + * @fires error + * @example + * const citiesSource = new carto.source.SQL('SELECT * FROM cities'); + * const citiesStyle = new carto.style.CartoCSS(` + * #layer { + * marker-fill: #FABADA; + * marker-width: 10; + * } + * `); + * // Create a layer with no options + * new carto.layer.Layer(citiesSource, citiesStyle); + * @example + * const citiesSource = new carto.source.SQL('SELECT * FROM cities'); + * const citiesStyle = new carto.style.CartoCSS(` + * #layer { + * marker-fill: #FABADA; + * marker-width: 10; + * } + * `); + * // Create a layer indicating what columns will be included in the featureOver event. + * new carto.layer.Layer(citiesSource, citiesStyle, { + * featureOverColumns: [ 'name' ] + * }); + * @example + * const citiesSource = new carto.source.SQL('SELECT * FROM cities'); + * const citiesStyle = new carto.style.CartoCSS(` + * #layer { + * marker-fill: #FABADA; + * marker-width: 10; + * } + * `); + * // Create a hidden layer + * new carto.layer.Layer(citiesSource, citiesStyle, { visible: false }); + * @example + * // Listen to the event thrown when the mouse is over a feature + * layer.on('featureOver', featureEvent => { + * console.log(`Mouse over city with name: ${featureEvent.data.name}`); + * }); + * @constructor + * @extends carto.layer.Base + * @memberof carto.layer + * @api + */ +function Layer (source, style, options = {}) { + Base.apply(this, arguments); + + _checkSource(source); + _checkStyle(style); + + this._client = undefined; + this._engine = undefined; + this._internalModel = undefined; + + this._source = source; + this._style = style; + this._visible = _.isBoolean(options.visible) ? options.visible : true; + this._featureClickColumns = options.featureClickColumns || []; + this._featureOverColumns = options.featureOverColumns || []; + this._minzoom = options.minzoom || 0; + this._maxzoom = options.maxzoom || undefined; + this._aggregation = options.aggregation || {}; + + _validateAggregationColumnsAndInteractivity(this._aggregation.columns, this._featureClickColumns, this._featureOverColumns); +} + +Layer.prototype = Object.create(Base.prototype); + +/** + * Set a new style for this layer. + * + * @param {carto.style.CartoCSS} style - New style + * @fires styleChanged + * @fires error + * @return {Promise} A promise that will be fulfilled when the style is applied to the layer or rejected with a + * {@link CartoError} if something goes bad + */ +Layer.prototype.setStyle = function (style, opts) { + var prevStyle = this._style; + _checkStyle(style); + opts = opts || {}; + if (prevStyle === style) { + return Promise.resolve(); + } + if (!this._internalModel) { + this._style = style; + this.trigger('styleChanged', this); + return Promise.resolve(); + } + // If style has an engine and is different from the layer`s engine throw an error + if (style.$getEngine() && style.$getEngine() !== this._internalModel._engine) { + throw new CartoValidationError('layer', 'differentStyleClient'); + } + // If style has no engine, set the layer engine in the style. + if (!style.$getEngine()) { + style.$setEngine(this._engine); + } + + this._internalModel.set('cartocss', style.getContent(), { silent: true }); + return this._engine.reload() + .then(function () { + this._style = style; + this.trigger('styleChanged', this); + }.bind(this)) + .catch(_rejectAndTriggerError.bind(this)); +}; + +/** + * Get the current style for this layer. + * + * @return {carto.style.CartoCSS} Current style + * @api + */ +Layer.prototype.getStyle = function () { + return this._style; +}; + +/** + * Set a new source for this layer. + * + * A source and a layer must belong to the same client so you can't + * add a source belonging to a different client. + * + * @param {carto.source.Base} source - New source + * @fires sourceChanged + * @fires error + * @return {Promise} A promise that will be fulfilled when the style is applied to the layer or rejected with a + * {@link CartoError} if something goes bad + */ +Layer.prototype.setSource = function (source) { + var prevSource = this._source; + _checkSource(source); + if (prevSource === source) { + return Promise.resolve(); + } + // If layer is not instantiated just store the new status + if (!this._internalModel) { + this._source = source; + this.trigger('sourceChanged', this); + return Promise.resolve(); + } + // If layer has been instantiated + // If the source already has an engine and is different from the layer's engine throw an error. + if (source.$getEngine() && source.$getEngine() !== this._internalModel._engine) { + throw new CartoValidationError('layer', 'differentSourceClient'); + } + // If source has no engine use the layer engine. + if (!source.$getEngine()) { + source.$setEngine(this._engine); + } + // Update the internalModel and return a promise + this._internalModel.set('source', source.$getInternalModel(), { silent: true }); + return this._engine.reload() + .then(function () { + this._source = source; + this.trigger('sourceChanged', this); + }.bind(this)) + .catch(_rejectAndTriggerError.bind(this)); +}; + +/** + * Get the current source for this layer. + * + * @return {carto.source.Base} Current source + * @api + */ +Layer.prototype.getSource = function () { + return this._source; +}; + +/** + * Set new columns for featureClick events. + * + * @param {Array} columns - An array containing column names + * @fires error + * @return {Promise} + * @api + */ +Layer.prototype.setFeatureClickColumns = function (columns) { + var prevColumns = this._featureClickColumns; + _checkColumns(columns); + if (_areColumnsTheSame(columns, prevColumns)) { + return Promise.resolve(); + } + // If layer is not instantiated just store the new status + if (!this._internalModel) { + this._featureClickColumns = columns; + return Promise.resolve(); + } + // Update the internalModel and return a promise + this._internalModel.infowindow.fields.set(_getInteractivityFields(columns).fields, { silent: true }); + return this._engine.reload() + .then(function () { + this._featureClickColumns = columns; + }.bind(this)) + .catch(_rejectAndTriggerError.bind(this)); +}; + +/** + * Get the columns available in featureClicked events. + * + * @return {Array} Column names available in featureClicked events + * @api + */ +Layer.prototype.getFeatureClickColumns = function () { + return this._featureClickColumns; +}; + +/** + * Set new columns for featureOver events. + * + * @param {Array} columns - An array containing column names + * @fires error + * @return {Promise} + * @api + */ +Layer.prototype.setFeatureOverColumns = function (columns) { + var prevColumns = this._featureOverColumns; + _checkColumns(columns); + if (_areColumnsTheSame(columns, prevColumns)) { + return Promise.resolve(); + } + // If layer is not instantiated just store the new status + if (!this._internalModel) { + this._featureOverColumns = columns; + return Promise.resolve(); + } + // Update the internalModel and return a promise + this._internalModel.tooltip.fields.set(_getInteractivityFields(columns).fields, { silent: true }); + return this._engine.reload() + .then(function () { + this._featureOverColumns = columns; + }.bind(this)) + .catch(_rejectAndTriggerError.bind(this)); +}; + +/** + * Get the columns available in featureOver events. + * + * @return {Array} Column names available in featureOver events + * @api + */ +Layer.prototype.getFeatureOverColumns = function () { + return this._featureOverColumns; +}; + +/** + * Hides the layer. + * + * @fires visibilityChanged + * @return {carto.layer.Layer} this + * @api + */ +Layer.prototype.hide = function () { + var prevStatus = this._visible; + this._visible = false; + if (this._internalModel) { + this._internalModel.set('visible', false); + } + if (prevStatus) { + this.trigger('visibilityChanged', false); + } + return this; +}; + +/** + * Shows the layer. + * + * @fires visibilityChanged + * @return {carto.layer.Layer} this + * @api + */ +Layer.prototype.show = function () { + var prevStatus = this._visible; + this._visible = true; + if (this._internalModel) { + this._internalModel.set('visible', true); + } + if (!prevStatus) { + this.trigger('visibilityChanged', false); + } + return this; +}; + +/** + * Change the layer's visibility. + * + * @fires visibilityChanged + * @return {carto.layer.Layer} this + */ +Layer.prototype.toggle = function () { + return this.isVisible() ? this.hide() : this.show(); +}; + +/** + * Return true if the layer is visible and false when not visible. + * + * @return {boolean} - A boolean value indicating the layer's visibility + * @api + */ +Layer.prototype.isVisible = function () { + return this._visible; +}; + +/** + * Return `true` if the layer is not visible and `false` when visible. + * + * @return {boolean} - A boolean value indicating the layer's visibility + * @api + */ +Layer.prototype.isHidden = function () { + return !this.isVisible(); +}; + +/** + * Return true if the layer has interactivity. + * + * @return {boolean} - A boolean value indicating the layer's interactivity + * @api + */ +Layer.prototype.isInteractive = function () { + return this.getFeatureClickColumns().length > 0 || this.getFeatureOverColumns().length > 0; +}; + +/** + * Set the layer's order. + * + * @param {number} index - new order index for the layer. + * + * @return {Promise} + * @api + */ +Layer.prototype.setOrder = function (index) { + if (!this._client) { + return Promise.resolve(); + } + return this._client.moveLayer(this, index); +}; + +/** + * Move the layer to the back. + * + * @return {Promise} + * @api + */ +Layer.prototype.bringToBack = function () { + return this.setOrder(0); +}; + +/** + * Move the layer to the front. + * + * @return {Promise} + * @api + */ +Layer.prototype.bringToFront = function () { + return this.setOrder(this._client._layers.size() - 1); +}; + +// Private functions. + +Layer.prototype._createInternalModel = function (engine) { + var internalModel = new CartoDBLayer({ + id: this._id, + source: this._source.$getInternalModel(), + cartocss: this._style.getContent(), + visible: this._visible, + infowindow: _getInteractivityFields(this._featureClickColumns), + tooltip: _getInteractivityFields(this._featureOverColumns), + minzoom: this._minzoom, + maxzoom: this._maxzoom + }, { + engine: engine, + aggregation: this._aggregation + }); + + internalModel.on('change:meta', function (layer, data) { + var rules = data.cartocss_meta.rules; + var styleMetadataList = metadataParser.getMetadataFromRules(rules); + + /** + * Event fired by {@link carto.layer.Layer} when the style contains any TurboCarto ramp. + * + * @typedef {object} carto.layer.MetadataEvent + * @property {carto.layer.metadata.Base[]} styles - List of style metadata objects + * @api + */ + var metadata = { styles: styleMetadataList }; + + this.trigger('metadataChanged', metadata); + }, this); + + internalModel.on('change:error', function (model, value) { + if (value && _isStyleError(value)) { + this._style.$setError(new CartoError(value)); + } else if (value) { + this.trigger(EVENTS.ERROR, new CartoError(value)); + } + }, this); + + return internalModel; +}; + +// Internal functions. + +Layer.prototype.$setClient = function (client) { + // Exit if the client is already set or + // it has a different engine than the layer + if (this._client || (this._engine && client._engine !== this._engine)) { + return; + } + this._client = client; +}; + +Layer.prototype.$setEngine = function (engine) { + if (this._engine) { + return; + } + this._engine = engine; + this._source.$setEngine(engine); + this._style.$setEngine(engine); + if (!this._internalModel) { + this._internalModel = this._createInternalModel(engine); + this._style.on('$changed', function (style) { + this._internalModel.set('cartocss', style.getContent(), { silent: true }); + }, this); + } +}; + +// Scope functions + +/** + * Transform the columns array into the format expected by the CartoDBLayer. + */ +function _getInteractivityFields (columns) { + var fields = columns.map(function (column, index) { + return { + name: column, + title: true, + position: index + }; + }); + + return { + fields: fields + }; +} + +function _checkStyle (style) { + if (!(style instanceof StyleBase)) { + throw new CartoValidationError('layer', 'nonValidStyle'); + } +} + +function _checkSource (source) { + if (!(source instanceof SourceBase)) { + throw new CartoValidationError('layer', 'nonValidSource'); + } +} + +function _checkColumns (columns) { + if (_.any(columns, function (item) { return !_.isString(item); })) { + throw new CartoValidationError('layer', 'nonValidColumns'); + } +} + +/** + * Return true when a windshaft error is because a styling error. + */ +function _isStyleError (windshaftError) { + return windshaftError.message && windshaftError.message.indexOf('style') >= 0; +} + +function _rejectAndTriggerError (err) { + var error = new CartoError(err); + this.trigger(EVENTS.ERROR, error); + return Promise.reject(error); +} + +function _areColumnsTheSame (newColumns, oldColumns) { + return newColumns.length === oldColumns.length && _.isEmpty(_.difference(newColumns, oldColumns)); +} + +/** + * When there are aggregated columns and interactivity columns they must agree + */ +function _validateAggregationColumnsAndInteractivity (aggregationColumns, clickColumns, overColumns) { + var aggColumns = (aggregationColumns && Object.keys(aggregationColumns)) || []; + + _validateColumnsConcordance(aggColumns, clickColumns, 'featureClick'); + _validateColumnsConcordance(aggColumns, overColumns, 'featureOver'); +} + +function _validateColumnsConcordance (aggColumns, interactivityColumns, interactivity) { + if (interactivityColumns.length > 0 && aggColumns.length > 0) { + var notInAggregation = _.filter(interactivityColumns, function (clickColumn) { + return !_.contains(aggColumns, clickColumn); + }); + + if (notInAggregation.length > 0) { + throw new CartoValidationError('layer', 'wrongInteractivityColumns[' + notInAggregation.join(', ') + ']#' + interactivity); + } + } +} + +/** + * @typedef {object} LatLng + * @property {number} lat - Latitude + * @property {number} lng - Longitude + * @api + */ + +/** + * Fired when the source has changed. Handler gets a parameter with the new source. + * + * @event sourceChanged + * @type {carto.layer.Layer} + * @api + */ + +/** + * Fired when the style has changed. Handler gets a parameter with the new style. + * + * @event styleChanged + * @type {carto.layer.Layer} + * @api + */ + +/** + * Fired when style metadata has changed. + * + * @event metadataChanged + * @type {carto.layer.MetadataEvent} + * @api + */ + +module.exports = Layer; diff --git a/src/api/v4/layer/metadata/base.js b/src/api/v4/layer/metadata/base.js new file mode 100644 index 0000000..4300f1e --- /dev/null +++ b/src/api/v4/layer/metadata/base.js @@ -0,0 +1,56 @@ +/** + * Base metadata object + * + * @constructor + * @abstract + * @memberof carto.layer.metadata + * @api + */ +function Base (type, rule) { + this._type = type || ''; + this._column = rule.getColumn(); + this._mapping = rule.getMapping(); + this._property = rule.getProperty(); +} + +/** + * Return the type of the metadata + * + * @return {string} + * @api + */ +Base.prototype.getType = function () { + return this._type; +}; + +/** + * Return the column of the metadata + * + * @return {string} + * @api + */ +Base.prototype.getColumn = function () { + return this._column; +}; + +/** + * Return the property of the metadata + * + * @return {string} + * @api + */ +Base.prototype.getMapping = function () { + return this._mapping; +}; + +/** + * Return the property of the metadata + * + * @return {string} + * @api + */ +Base.prototype.getProperty = function () { + return this._property; +}; + +module.exports = Base; diff --git a/src/api/v4/layer/metadata/buckets.js b/src/api/v4/layer/metadata/buckets.js new file mode 100644 index 0000000..3cc9892 --- /dev/null +++ b/src/api/v4/layer/metadata/buckets.js @@ -0,0 +1,95 @@ +var Base = require('./base'); + +/** + * Metadata type buckets + * + * Adding a Turbocarto ramp (with ranges) in the style generates a response + * from the server with the resulting information, after computing the ramp. + * This information is wrapped in a metadata object of type 'buckets', that + * contains a list of buckets with the range (min, max) and the value. And + * also the total min, max range and the average of the total values. + * + * For example, the following ramp will generate a metadata of type 'buckets' + * with numeric values (the size) in its buckets: + * + * marker-width: ramp([scalerank], range(5, 20), quantiles(5)); + * + * In another example, this ramp will generate a metadata of type 'buckets' + * with string values (the color) in its buckets: + * + * marker-fill: ramp([scalerank], (#FFC6C4, #EE919B, #CC607D), quantiles); + * + * @param {object} rule - Rule with the cartocss metadata + * @constructor + * @hideconstructor + * @extends carto.layer.metadata.Base + * @memberof carto.layer.metadata + * @api + */ +function Buckets (rule) { + var rangeBuckets = rule.getBucketsWithRangeFilter(); + + /** + * @typedef {object} carto.layer.metadata.Bucket + * @property {number} min - The minimum range value + * @property {number} max - The maximum range value + * @property {number|string} value - The value of the bucket + * @api + */ + this._buckets = rangeBuckets.map(function (bucket) { + return { + min: bucket.filter.start, + max: bucket.filter.end, + value: bucket.value + }; + }); + this._avg = rule.getFilterAvg(); + this._min = rangeBuckets.length > 0 ? rangeBuckets[0].filter.start : undefined; + this._max = rangeBuckets.length > 0 ? rangeBuckets[rangeBuckets.length - 1].filter.end : undefined; + + Base.call(this, 'buckets', rule); +} + +Buckets.prototype = Object.create(Base.prototype); + +/** + * Return the buckets + * + * @return {carto.layer.metadata.Bucket[]} + * @api + */ +Buckets.prototype.getBuckets = function () { + return this._buckets; +}; + +/** + * Return the average of the column + * + * @return {number} + * @api + */ +Buckets.prototype.getAverage = function () { + return this._avg; +}; + +/** + * Return the minimum value in the ranges + * + * @return {number} + * @api + */ +Buckets.prototype.getMin = function () { + return this._min; +}; + +/** + * Return the maximum value in the ranges + * + * @return {number} + * @api + */ +Buckets.prototype.getMax = function () { + return this._max; +}; + +module.exports = Buckets; diff --git a/src/api/v4/layer/metadata/categories.js b/src/api/v4/layer/metadata/categories.js new file mode 100644 index 0000000..1f3e991 --- /dev/null +++ b/src/api/v4/layer/metadata/categories.js @@ -0,0 +1,68 @@ +var Base = require('./base'); + +/** + * Metadata type categories + * + * Adding a Turbocarto ramp (with categories) in the style generates a response + * from the server with the resulting information after computing the ramp. + * This information is wrapped in a metadata object of type 'categories', that + * contains a list of categories with the name of the category and the value. And + * also the default value if it has been defined in the ramp. + * + * For example, the following ramp will generate a metadata of type 'categories' + * with string values (the color) in its categories. The #CCCCCC is the default + * value in this case: + * + * marker-fill: ramp([scalerank], (#F54690, #D16996, #CCCCCC), (1, 2), "=", category); + * + * @param {object} rule - Rule with the cartocss metadata + * @constructor + * @hideconstructor + * @extends carto.layer.metadata.Base + * @memberof carto.layer.metadata + * @api + */ +function Categories (rule) { + var categoryBuckets = rule.getBucketsWithCategoryFilter(); + var defaultBuckets = rule.getBucketsWithDefaultFilter(); + + /** + * @typedef {object} carto.layer.metadata.Category + * @property {number|string} name - The name of the category + * @property {string} value - The value of the category + * @api + */ + this._categories = categoryBuckets.map(function (bucket) { + return { + name: bucket.filter.name, + value: bucket.value + }; + }); + this._defaultValue = defaultBuckets.length > 0 ? defaultBuckets[0].value : undefined; + + Base.call(this, 'categories', rule); +} + +Categories.prototype = Object.create(Base.prototype); + +/** + * Return the buckets + * + * @return {carto.layer.metadata.Category[]} + * @api + */ +Categories.prototype.getCategories = function () { + return this._categories; +}; + +/** + * Return the default value + * + * @return {string} + * @api + */ +Categories.prototype.getDefaultValue = function () { + return this._defaultValue; +}; + +module.exports = Categories; diff --git a/src/api/v4/layer/metadata/parser.js b/src/api/v4/layer/metadata/parser.js new file mode 100644 index 0000000..fe9cf8a --- /dev/null +++ b/src/api/v4/layer/metadata/parser.js @@ -0,0 +1,37 @@ +var BucketsMetadata = require('./buckets'); +var CategoriesMetadata = require('./categories'); +var Rule = require('../../../../windshaft-integration/legends/rule.js'); + +/** + * Generates a list of Metadata objects from the original cartocss_meta rules + * + * @param {Rules} rulesData + * @return {metadata.Base[]} + */ +function getMetadataFromRules (rulesData) { + var metadata = []; + + rulesData.forEach(function (ruleData) { + var rule = new Rule(ruleData); + + if (_isBucketsMetadata(rule)) { + metadata.push(new BucketsMetadata(rule)); + } else if (_isCategoriesMetadata(rule)) { + metadata.push(new CategoriesMetadata(rule)); + } + }); + + return metadata; +} + +function _isBucketsMetadata (rule) { + return rule.getBucketsWithRangeFilter().length > 0; +} + +function _isCategoriesMetadata (rule) { + return rule.getBucketsWithCategoryFilter().length > 0; +} + +module.exports = { + getMetadataFromRules: getMetadataFromRules +}; diff --git a/src/api/v4/layers.js b/src/api/v4/layers.js new file mode 100644 index 0000000..e70f653 --- /dev/null +++ b/src/api/v4/layers.js @@ -0,0 +1,45 @@ +var _ = require('underscore'); + +function Layers (layers) { + this._layers = layers || []; +} + +Layers.prototype.add = function (layer) { + this._layers.push(layer); + return layer; +}; + +Layers.prototype.remove = function (layer) { + return this._layers.splice(this._layers.indexOf(layer), 1); +}; + +Layers.prototype.size = function () { + return this._layers.length; +}; + +Layers.prototype.indexOf = function (layer) { + return this._layers.indexOf(layer); +}; + +Layers.prototype.contains = function (layer) { + return this._layers.indexOf(layer) >= 0; +}; + +Layers.prototype.findById = function (layerId) { + return _.find(this._layers, function (layer) { + return layer.getId() === layerId; + }, this); +}; + +Layers.prototype.toArray = function () { + return this._layers; +}; + +Layers.prototype.move = function (layer, toIndex) { + var fromIndex = this._layers.indexOf(layer); + if (fromIndex >= 0 && fromIndex !== toIndex) { + this._layers.splice(toIndex, 0, this._layers.splice(fromIndex, 1)[0]); + } +}; + +module.exports = Layers; diff --git a/src/api/v4/native/google-maps-map-type.js b/src/api/v4/native/google-maps-map-type.js new file mode 100644 index 0000000..fd17db8 --- /dev/null +++ b/src/api/v4/native/google-maps-map-type.js @@ -0,0 +1,70 @@ +/* global google */ +var _ = require('underscore'); +var Layer = require('../layer'); +var triggerLayerFeatureEvent = require('./trigger-layer-feature-event'); +var GMapsCartoDBLayerGroupView = require('../../../geo/gmaps/gmaps-cartodb-layer-group-view'); +var CartoError = require('../error-handling/carto-error'); + +/** + * This object is a custom Google Maps MapType to enable feature interactivity + * using an internal GMapsCartoDBLayerGroupView instance. + * + * NOTE: It also contains the feature events handlers. That's why it requires the carto layers array. + */ +function GoogleMapsMapType (layers, engine, map) { + this._layers = layers; + this._engine = engine; + this._map = map; + + this._hoveredLayers = []; + + this.tileSize = new google.maps.Size(256, 256); + this._internalView = new GMapsCartoDBLayerGroupView(this._engine._cartoLayerGroup, { + nativeMap: map + }); + this._id = this._internalView._id; + this._internalView.on('featureClick', this._onFeatureClick, this); + this._internalView.on('featureOver', this._onFeatureOver, this); + this._internalView.on('featureOut', this._onFeatureOut, this); + this._internalView.on('featureError', this._onFeatureError, this); +} + +GoogleMapsMapType.prototype.getTile = function (coord, zoom, ownerDocument) { + return this._internalView.getTile(coord, zoom, ownerDocument); +}; + +GoogleMapsMapType.prototype._onFeatureClick = function (internalEvent) { + var layer = this._layers.findById(internalEvent.layer.id); + triggerLayerFeatureEvent(Layer.events.FEATURE_CLICKED, internalEvent, layer); +}; + +GoogleMapsMapType.prototype._onFeatureOver = function (internalEvent) { + var layer = this._layers.findById(internalEvent.layer.id); + if (layer.isInteractive()) { + this._hoveredLayers[internalEvent.layerIndex] = true; + this._map.setOptions({ draggableCursor: 'pointer' }); + } + triggerLayerFeatureEvent(Layer.events.FEATURE_OVER, internalEvent, layer); +}; + +GoogleMapsMapType.prototype._onFeatureOut = function (internalEvent) { + var layer = this._layers.findById(internalEvent.layer.id); + this._hoveredLayers[internalEvent.layerIndex] = false; + if (_.any(this._hoveredLayers)) { + this._map.setOptions({ draggableCursor: 'pointer' }); + } else { + this._map.setOptions({ draggableCursor: 'auto' }); + } + triggerLayerFeatureEvent(Layer.events.FEATURE_OUT, internalEvent, layer); +}; + +GoogleMapsMapType.prototype._onFeatureError = function (error) { + var cartoError = new CartoError(error); + _.each(this._layers.toArray(), function (layer) { + if (layer.isInteractive()) { + layer.trigger(Layer.events.TILE_ERROR, cartoError); + } + }); +}; + +module.exports = GoogleMapsMapType; diff --git a/src/api/v4/native/leaflet-layer.js b/src/api/v4/native/leaflet-layer.js new file mode 100644 index 0000000..5feaf3c --- /dev/null +++ b/src/api/v4/native/leaflet-layer.js @@ -0,0 +1,107 @@ +/* global L */ +var _ = require('underscore'); +var Layer = require('../layer'); +var constants = require('../constants'); +var triggerLayerFeatureEvent = require('./trigger-layer-feature-event'); +var LeafletCartoLayerGroupView = require('../../../geo/leaflet/leaflet-cartodb-layer-group-view'); +var CartoError = require('../error-handling/carto-error'); + +/** + * This object is a custom Leaflet layer to enable feature interactivity + * using an internal LeafletCartoLayerGroupView instance. + * + * There are two overwritten functions: + * - addTo: when the layer is added to a map it also creates a LeafletCartoLayerGroupView + * object called `_internalView` in order to enable the feature events + * - removeFrom: when the layer is removed from a map it also removes the feature events + * listeners, triggers a 'remove' event and removes the `_internalView` + * + * NOTE: It also contains the feature events handlers. That's why it requires the carto layers array. + */ +var LeafletLayer = L.TileLayer.extend({ + options: { + opacity: 0.99, + maxZoom: 30, + attribution: constants.ATTRIBUTION + }, + + initialize: function (layers, engine, options) { + _.extend(this.options, options); + + this._layers = layers; + this._engine = engine; + this._internalView = null; + + this._hoveredLayers = []; + }, + + addTo: function (map) { + if (!this._internalView) { + this._internalView = new LeafletCartoLayerGroupView(this._engine._cartoLayerGroup, { + nativeMap: map, + nativeLayer: this + }); + this._internalView.on('featureClick', this._onFeatureClick, this); + this._internalView.on('featureOver', this._onFeatureOver, this); + this._internalView.on('featureOut', this._onFeatureOut, this); + this._internalView.on('featureError', this._onFeatureError, this); + } + + return L.TileLayer.prototype.addTo.call(this, map); + }, + + removeFrom: function (map) { + if (this._internalView) { + this._internalView.off('featureClick'); + this._internalView.off('featureOver'); + this._internalView.off('featureOut'); + this._internalView.off('featureError'); + this._internalView.notifyRemove(); + } + this._internalView = null; + + return L.TileLayer.prototype.removeFrom.call(this, map); + }, + + setUrl: undefined, + + _setUrl: function (url, noDraw) { + return L.TileLayer.prototype.setUrl.call(this, url, noDraw); + }, + + _onFeatureClick: function (internalEvent) { + var layer = this._layers.findById(internalEvent.layer.id); + triggerLayerFeatureEvent(Layer.events.FEATURE_CLICKED, internalEvent, layer); + }, + + _onFeatureOver: function (internalEvent) { + var layer = this._layers.findById(internalEvent.layer.id); + if (layer.isInteractive()) { + this._hoveredLayers[internalEvent.layerIndex] = true; + this._map.getContainer().style.cursor = 'pointer'; + } + triggerLayerFeatureEvent(Layer.events.FEATURE_OVER, internalEvent, layer); + }, + + _onFeatureOut: function (internalEvent) { + var layer = this._layers.findById(internalEvent.layer.id); + this._hoveredLayers[internalEvent.layerIndex] = false; + if (_.any(this._hoveredLayers)) { + this._map.getContainer().style.cursor = 'pointer'; + } else { + this._map.getContainer().style.cursor = 'auto'; + } + triggerLayerFeatureEvent(Layer.events.FEATURE_OUT, internalEvent, layer); + }, + + _onFeatureError: function (error) { + var cartoError = new CartoError(error); + _.each(this._layers.toArray(), function (layer) { + if (layer.isInteractive()) { + layer.trigger(Layer.events.TILE_ERROR, cartoError); + } + }); + } +}); + +module.exports = LeafletLayer; diff --git a/src/api/v4/native/trigger-layer-feature-event.js b/src/api/v4/native/trigger-layer-feature-event.js new file mode 100644 index 0000000..d422249 --- /dev/null +++ b/src/api/v4/native/trigger-layer-feature-event.js @@ -0,0 +1,57 @@ +module.exports = function (eventName, internalEvent, layer) { + if (layer) { + var event = { + data: undefined, + latLng: undefined + }; + if (internalEvent.feature) { + event.data = internalEvent.feature; + } + if (internalEvent.latlng) { + event.latLng = { + lat: internalEvent.latlng[0], + lng: internalEvent.latlng[1] + }; + } + if (internalEvent.position) { + event.position = { + x: internalEvent.position.x, + y: internalEvent.position.y + }; + } + + /** + * Event object for feature events triggered by {@link carto.layer.Layer}. + * + * @typedef {object} carto.layer.FeatureEvent + * @property {LatLng} latLng - Object with coordinates where interaction took place + * @property {object} data - Object with feature data (one attribute for each specified column) + * @api + */ + + /** + * Fired when user clicks on a feature. + * + * @event featureClicked + * @type {carto.layer.FeatureEvent} + * @api + */ + + /** + * Fired when user moves the mouse over a feature. + * + * @event featureOver + * @type {carto.layer.FeatureEvent} + * @api + */ + + /** + * Fired when user moves the mouse out of a feature. + * + * @event featureOut + * @type {carto.layer.FeatureEvent} + * @api + */ + layer.trigger(eventName, event); + } +}; diff --git a/src/api/v4/source/base.js b/src/api/v4/source/base.js new file mode 100644 index 0000000..ab58e04 --- /dev/null +++ b/src/api/v4/source/base.js @@ -0,0 +1,137 @@ +const _ = require('underscore'); +const Backbone = require('backbone'); +const CartoError = require('../error-handling/carto-error'); +const FiltersCollection = require('../filter/filters-collection'); +const EVENTS = require('../events'); + +/** + * Base data source object. + * + * The methods listed in the {@link carto.source.Base|source.Base} object are available in all source objects. + * + * Use a source to reference the data used in a {@link carto.dataview.Base|dataview} or a {@link carto.layer.Base|layer}. + * + * {@link carto.source.Base} should not be used directly use {@link carto.source.Dataset} or {@link carto.source.SQL} instead. + * + * @constructor + * @fires error + * @abstract + * @memberof carto.source + * @api + */ +function Base () { + this._id = Base.$generateId(); + this._hasFiltersApplied = false; + this._appliedFilters = new FiltersCollection(); +} + +_.extend(Base.prototype, Backbone.Events); + +/** + * The instance id will be autogenerated by incrementing this variable. + */ +Base.$nextId = 0; + +/** + * Static funciton used internally to autogenerate source ids. + */ +Base.$generateId = function () { + return 'S' + ++Base.$nextId; +}; + +/** + * Return a unique autogenerated id. + * + * @return {string} Unique autogenerated id + */ +Base.prototype.getId = function () { + return this._id; +}; + +Base.prototype._createInternalModel = function (engine) { + throw new Error('_createInternalModel must be implemented by the particular source'); +}; + +/** + * Fire a CartoError event from a internalError + */ +Base.prototype._triggerError = function (model, internalError) { + this.trigger(EVENTS.ERROR, new CartoError(internalError, { analysis: this })); +}; + +Base.prototype.$setEngine = function (engine) { + if (!this._internalModel) { + this._internalModel = this._createInternalModel(engine); + this._internalModel.on('change:error', this._triggerError, this); + } +}; + +/** + * Return the engine form the source internal model + */ +Base.prototype.$getEngine = function (engine) { + if (this._internalModel) { + return this._internalModel._engine; + } +}; + +/** + * Return the real CARTO.js model used by the source. + */ +Base.prototype.$getInternalModel = function () { + return this._internalModel; +}; + +/** + * Get added filters + * + * @returns {Array} Added filters + * @api + */ +Base.prototype.getFilters = function () { + return this._appliedFilters.getFilters(); +}; + +/** + * Add new filter to the source + * + * @param {(carto.filter.Range|carto.filter.Category|carto.filter.AND|carto.filter.OR)} filter + * @api + */ +Base.prototype.addFilter = function (filter) { + this._hasFiltersApplied = true; + this._appliedFilters.addFilter(filter); +}; + +/** + * Add new filters to the source + * + * @param {Array} filters + * @api + */ +Base.prototype.addFilters = function (filters) { + filters.forEach(filter => this.addFilter(filter)); +}; + +/** + * Remove an existing filter from source + * + * @param {(carto.filter.Range|carto.filter.Category|carto.filter.AND|carto.filter.OR)} filter + * @api + */ +Base.prototype.removeFilter = function (filter) { + this._appliedFilters.removeFilter(filter); + this._hasFiltersApplied = Boolean(this._appliedFilters.count()); +}; + +/** + * Remove existing filters from source + * + * @param {Array} filters + * @api + */ +Base.prototype.removeFilters = function (filters) { + filters.forEach(filter => this.removeFilter(filter)); +}; + +module.exports = Base; diff --git a/src/api/v4/source/dataset.js b/src/api/v4/source/dataset.js new file mode 100644 index 0000000..82151e1 --- /dev/null +++ b/src/api/v4/source/dataset.js @@ -0,0 +1,128 @@ +var _ = require('underscore'); +var Base = require('./base'); +var AnalysisModel = require('../../../analysis/analysis-model'); +var CamshaftReference = require('../../../analysis/camshaft-reference'); +var CartoValidationError = require('../error-handling/carto-validation-error'); +var CartoError = require('../error-handling/carto-error'); + +/** + * A Dataset that can be used as the data source for layers and dataviews. + * + * @param {string} tableName The name of an existing table + * @example + * new carto.source.Dataset('european_cities'); + * @constructor + * @fires error + * @extends carto.source.Base + * @memberof carto.source + * @api + */ +function Dataset (tableName) { + _checkTableName(tableName); + this._tableName = tableName; + + Base.apply(this, arguments); + + this._appliedFilters.on('change:filters', () => this._updateInternalModelQuery(this._getQueryToApply())); +} + +Dataset.prototype = Object.create(Base.prototype); + +/** + * Update the table name. This method is asyncronous and returns a promise which is resolved when the style + * is changed succesfully. It also fires a 'tableNameChanged' event. + * + * @param {string} tableName The name of an existing table + * @fires TableNameChanged + * @returns {Promise} - A promise that will be fulfilled when the reload cycle is completed + * @api + */ +Dataset.prototype.setTableName = function (tableName) { + _checkTableName(tableName); + + this._tableName = tableName; + + if (!this._internalModel) { + this._triggerTableNameChanged(this, tableName); + return Promise.resolve(); + } + + return this._updateInternalModelQuery(this._getQueryToApply()); +}; + +/** + * Return the table name being used in this Dataset object. + * + * @return {string} The table name being used in this Dataset object + * @api + */ +Dataset.prototype.getTableName = function () { + return this._tableName; +}; + +/** + * Creates a new internal model with the given engine and attributes initialized in the constructor. + * + * @param {Engine} engine - The engine object to be assigned to the internalModel + */ +Dataset.prototype._createInternalModel = function (engine) { + var internalModel = new AnalysisModel({ + id: this.getId(), + type: 'source', + query: this._getQueryToApply() + }, { + camshaftReference: CamshaftReference, + engine: engine + }); + + return internalModel; +}; + +Dataset.prototype._updateInternalModelQuery = function (query) { + if (!this._internalModel) return; + + this._internalModel.set('query', query, { silent: true }); + + return this._internalModel._engine.reload() + .then(() => this._triggerTableNameChanged(this, this._tableName)) + .catch(windshaftError => Promise.reject(new CartoError(windshaftError))); +}; + +Dataset.prototype._getQueryToApply = function () { + const whereClause = this._appliedFilters.$getSQL(); + const datasetQuery = `SELECT * from ${this._tableName}`; + + if (_.isEmpty(whereClause)) { + return datasetQuery; + } + + return `SELECT * FROM (${datasetQuery}) as datasetQuery WHERE ${whereClause}`; +}; + +Dataset.prototype.addFilter = function (filter) { + Base.prototype.addFilter.apply(this, arguments); + this._updateInternalModelQuery(this._getQueryToApply()); +}; + +Dataset.prototype.removeFilter = function (filters) { + Base.prototype.removeFilter.apply(this, arguments); + this._updateInternalModelQuery(this._getQueryToApply()); +}; + +Dataset.prototype._triggerTableNameChanged = function (model, value) { + this.trigger('tableNameChanged', value); +}; + +function _checkTableName (tableName) { + if (_.isUndefined(tableName)) { + throw new CartoValidationError('source', 'noDatasetName'); + } + if (!_.isString(tableName)) { + throw new CartoValidationError('source', 'requiredDatasetString'); + } + if (_.isEmpty(tableName)) { + throw new CartoValidationError('source', 'requiredDataset'); + } +} + +module.exports = Dataset; diff --git a/src/api/v4/source/index.js b/src/api/v4/source/index.js new file mode 100644 index 0000000..5eab5a7 --- /dev/null +++ b/src/api/v4/source/index.js @@ -0,0 +1,11 @@ +var Dataset = require('./dataset'); +var SQL = require('./sql'); + +/** + * @namespace carto.source + * @api + */ +module.exports = { + Dataset: Dataset, + SQL: SQL +}; diff --git a/src/api/v4/source/sql.js b/src/api/v4/source/sql.js new file mode 100644 index 0000000..4a76941 --- /dev/null +++ b/src/api/v4/source/sql.js @@ -0,0 +1,137 @@ +var _ = require('underscore'); +var Base = require('./base'); +var AnalysisModel = require('../../../analysis/analysis-model'); +var CamshaftReference = require('../../../analysis/camshaft-reference'); +var CartoValidationError = require('../error-handling/carto-validation-error'); +var CartoError = require('../error-handling/carto-error'); + +/** + * A SQL Query that can be used as the data source for layers and dataviews. + * + * @param {string} query A SQL query containing a SELECT statement + * @fires error + * @example + * new carto.source.SQL('SELECT * FROM european_cities'); + * @constructor + * @extends carto.source.Base + * @memberof carto.source + * @fires queryChanged + * @api + */ +function SQL (query) { + _checkQuery(query); + this._query = query; + + Base.apply(this, arguments); + + this._appliedFilters.on('change:filters', () => this._updateInternalModelQuery(this._getQueryToApply())); +} + +SQL.prototype = Object.create(Base.prototype); + +/** + * Update the query. This method is asyncronous and returns a promise which is resolved when the style + * is changed succesfully. It also fires a 'queryChanged' event. + * + * @param {string} query - The sql query that will be the source of the data + * @fires queryChanged + * @returns {Promise} - A promise that will be fulfilled when the reload cycle is completed + * @api + */ +SQL.prototype.setQuery = function (query) { + _checkQuery(query); + this._query = query; + + const sqlString = this._getQueryToApply(); + + if (!this._internalModel) { + this._triggerQueryChanged(this, sqlString); + return Promise.resolve(); + } + + return this._updateInternalModelQuery(sqlString); +}; + +/** + * Get the query being used in this SQL source. + * + * @return {string} The query being used in this SQL object + * @api + */ +SQL.prototype.getQuery = function () { + return this._query; +}; + +/** + * Creates a new internal model with the given engine and attributes initialized in the constructor. + * + * @param {Engine} engine - The engine object to be assigned to the internalModel + */ +SQL.prototype._createInternalModel = function (engine) { + var internalModel = new AnalysisModel({ + id: this.getId(), + type: 'source', + query: this._getQueryToApply() + }, { + camshaftReference: CamshaftReference, + engine: engine + }); + + internalModel.on('change:query', this._triggerQueryChanged, this); + + return internalModel; +}; + +SQL.prototype._updateInternalModelQuery = function (query) { + if (!this._internalModel) return; + + this._internalModel.set('query', query, { silent: true }); + + return this._internalModel._engine.reload() + .then(() => this._triggerQueryChanged(this, query)) + .catch(windshaftError => Promise.reject(new CartoError(windshaftError))); +}; + +SQL.prototype._getQueryToApply = function () { + const whereClause = this._appliedFilters.$getSQL(); + + if (!this._hasFiltersApplied || _.isEmpty(whereClause)) { + return this._query; + } + + return `SELECT * FROM (${this._query}) as originalQuery WHERE ${whereClause}`; +}; + +SQL.prototype.addFilter = function (filter) { + Base.prototype.addFilter.apply(this, arguments); + this._updateInternalModelQuery(this._getQueryToApply()); +}; + +SQL.prototype.removeFilter = function (filters) { + Base.prototype.removeFilter.apply(this, arguments); + this._updateInternalModelQuery(this._getQueryToApply()); +}; + +SQL.prototype._triggerQueryChanged = function (model, value) { + this.trigger('queryChanged', value); +}; + +function _checkQuery (query) { + if (!query) { + throw new CartoValidationError('source', 'requiredQuery'); + } + + if (!_.isString(query)) { + throw new CartoValidationError('source', 'requiredString'); + } +} + +module.exports = SQL; + +/** + * Fired when the query has changed. Handler gets a parameter with the new query. + * + * @event queryChanged + * @type {string} + * @api + */ diff --git a/src/api/v4/style/base.js b/src/api/v4/style/base.js new file mode 100644 index 0000000..33d2b67 --- /dev/null +++ b/src/api/v4/style/base.js @@ -0,0 +1,33 @@ +var _ = require('underscore'); +var Backbone = require('backbone'); + +/** + * Base style object. + * + * @fires error + * @constructor + * @abstract + * @memberof carto.style + * @api + */ +function Base () {} + +_.extend(Base.prototype, Backbone.Events); + +Base.prototype.$setError = function (cartoError) { + this._error = cartoError; + this.trigger('error', cartoError); +}; + +Base.prototype.$setEngine = function (newEngine) { + if (this._engine && this._engine !== newEngine) { + throw new Error('CartoCSS engine cannot be changed'); + } + this._engine = newEngine; +}; + +Base.prototype.$getEngine = function () { + return this._engine; +}; + +module.exports = Base; diff --git a/src/api/v4/style/cartocss.js b/src/api/v4/style/cartocss.js new file mode 100644 index 0000000..a1cc7f8 --- /dev/null +++ b/src/api/v4/style/cartocss.js @@ -0,0 +1,96 @@ +var _ = require('underscore'); +var Base = require('./base'); +var CartoValidationError = require('../error-handling/carto-validation-error'); +var CartoError = require('../error-handling/carto-error'); + +// Event constants +var CONTENT_CHANGED = 'contentChanged'; + +/** + * A CartoCSS/TurboCarto style that can be applied to a {@link carto.layer.Layer}. + * @param {string} content - A CartoCSS string + * @example + * var style = new carto.style.CartoCSS(` + * #layer { + * marker-fill: #FABADA; + * marker-width: 10; + * } + * `); + * @constructor + * @extends carto.style.Base + * @memberof carto.style + * @api + */ +function CartoCSS (content) { + _checkContent(content); + this._content = content; +} + +CartoCSS.prototype = Object.create(Base.prototype); + +/** + * Get the current CartoCSS/TurboCarto style as a string. + * + * @return {string} - The TurboCarto style for this CartoCSS object + * @api + */ +CartoCSS.prototype.getContent = function () { + return this._content; +}; + +/** + * Set the CartoCSS/Turbocarto as a string. + * + * @param {string} newContent - A string containing the new cartocss/turbocarto style + * @return {Promise} A promise that will be resolved once the cartocss/turbocarto is updated + * @example + * // Get the cartoCSS from an exiting layer + * let cartoCSS = layer.getStyle(); + * // Update the cartoCSS content, remember this method is asynchronous! + * cartoCSS.setContent(` + * #layer { + * marker-fill: blue; + * }`) + * .then(() => { + * console.log('cartoCSS was updated'); + * }) + * .catch(() => { + * console.error('Error updating the cartoCSS for the layer'); + * }); + * @api + */ +CartoCSS.prototype.setContent = function (newContent) { + _checkContent(newContent); + this._content = newContent; + // Notify layers that the style has been changed so they can update their internalModels. + this.trigger('$changed', this); + if (!this._engine) { + return _onContentChanged.call(this, newContent); + } + + return this._engine.reload() + .then(function () { + return _onContentChanged.call(this, newContent); + }.bind(this)) + .catch(function (windshaftError) { + return Promise.reject(new CartoError(windshaftError)); + }); +}; + +// Once the reload cycle is completed trigger a contentChanged event. +function _onContentChanged (newContent) { + this.trigger(CONTENT_CHANGED, this._content); + return Promise.resolve(this._content); +} + +function _checkContent (content) { + if (!content) { + throw new CartoValidationError('style', 'requiredCSS'); + } + + if (!_.isString(content)) { + throw new CartoValidationError('style', 'requiredCSSString'); + } +} + +module.exports = CartoCSS; diff --git a/src/api/v4/style/index.js b/src/api/v4/style/index.js new file mode 100644 index 0000000..73256b5 --- /dev/null +++ b/src/api/v4/style/index.js @@ -0,0 +1,9 @@ +var CartoCSS = require('./cartocss'); + +/** + * @namespace carto.style + * @api + */ +module.exports = { + CartoCSS: CartoCSS +}; diff --git a/src/api/vizjson.js b/src/api/vizjson.js new file mode 100644 index 0000000..6ac3d39 --- /dev/null +++ b/src/api/vizjson.js @@ -0,0 +1,133 @@ +var _ = require('underscore'); +var log = require('cdb.log'); +var C = require('../constants'); + +var VizJSON = function (vizjson) { + _.each(Object.keys(vizjson), function (property) { + this[property] = vizjson[property]; + }, this); + + this.overlays = this.overlays || []; + this.layers = this.layers || []; + + this._addAttributionOverlay(); +}; + +VizJSON.prototype.isNamedMap = function () { + return !!this.datasource.template_name; +}; + +VizJSON.prototype.hasZoomOverlay = function () { + return this.hasOverlay(C.OVERLAY_TYPES.ZOOM); +}; + +VizJSON.prototype.hasOverlay = function (overlayType) { + return _.isObject(this.getOverlayByType(overlayType)); +}; + +VizJSON.prototype.getOverlayByType = function (overlayType) { + return _.find(this.overlays, function (overlay) { + return overlay.type === overlayType; + }); +}; + +VizJSON.prototype.addHeaderOverlay = function (showTitle, showDescription, isShareable) { + if (!this.hasOverlay(C.OVERLAY_TYPES.HEADER)) { + this.overlays.unshift({ + type: C.OVERLAY_TYPES.HEADER, + order: 1, + shareable: isShareable, + url: this.url, + options: { + extra: { + title: this.title, + description: this.description, + show_title: showTitle, + show_description: showDescription + } + } + }); + } +}; + +VizJSON.prototype.addSearchOverlay = function () { + if (!this.hasOverlay(C.OVERLAY_TYPES.SEARCH)) { + this.overlays.push({ + type: C.OVERLAY_TYPES.SEARCH, + order: 3 + }); + } +}; + +VizJSON.prototype.removeOverlay = function (overlayType) { + for (var i = 0; i < this.overlays.length; ++i) { + if (this.overlays[i].type === overlayType) { + this.overlays.splice(i, 1); + return; + } + } +}; + +VizJSON.prototype.removeLoaderOverlay = function () { + this.removeOverlay(C.OVERLAY_TYPES.LOADER); +}; + +VizJSON.prototype.removeZoomOverlay = function () { + this.removeOverlay(C.OVERLAY_TYPES.ZOOM); +}; + +VizJSON.prototype.removeSearchOverlay = function () { + this.removeOverlay(C.OVERLAY_TYPES.SEARCH); +}; + +VizJSON.prototype.removeLogoOverlay = function (overlayType) { + this.removeOverlay(C.OVERLAY_TYPES.LOGO); +}; + +VizJSON.prototype._addAttributionOverlay = function () { + this.overlays.push({ + type: C.OVERLAY_TYPES.ATTRIBUTION + }); +}; + +VizJSON.prototype.enforceGMapsBaseLayer = function (gmapsBaseType, gmapsStyle) { + var isGmapsBaseTypeValid = _.contains(C.GMAPS_BASE_LAYER_TYPES, gmapsBaseType); + if (this.map_provider === C.MAP_PROVIDER_TYPES.LEAFLET && isGmapsBaseTypeValid) { + if (this.layers) { + this.layers[0].options.type = 'GMapsBase'; + this.layers[0].options.baseType = gmapsBaseType; + this.layers[0].options.name = gmapsBaseType; + + if (gmapsStyle) { + this.layers[0].options.style = typeof gmapsStyle === 'string' ? JSON.parse(gmapsStyle) : gmapsStyle; + } + + this.map_provider = C.MAP_PROVIDER_TYPES.GMAPS; + this.layers[0].options.attribution = ''; // GMaps has its own attribution + } else { + log.error('No base map loaded. Using Leaflet.'); + } + } else { + log.error('GMaps baseType "' + gmapsBaseType + ' is not supported. Using leaflet.'); + } +}; + +VizJSON.prototype.setZoom = function (zoom) { + this.zoom = zoom; + this.bounds = null; +}; + +VizJSON.prototype.setCenter = function (center) { + this.center = center; + this.bounds = null; +}; + +VizJSON.prototype.setBounds = function (bounds) { + this.bounds = bounds; +}; + +VizJSON.prototype.setVector = function (vector) { + this.vector = vector; +}; + +module.exports = VizJSON; diff --git a/src/cartodb.js b/src/cartodb.js new file mode 100644 index 0000000..4f6c074 --- /dev/null +++ b/src/cartodb.js @@ -0,0 +1,27 @@ +window.L = require('leaflet'); + +require('mousewheel'); // registers itself to $.event; TODO what's this required for? still relevant for supported browsers? +require('mwheelIntent'); // registers itself to $.event; TODO what's this required for? still relevant for supported browsers? + +var cdb = require('cdb'); +if (window) { + window.cartodb = window.cdb = cdb; +} + +cdb.core = {}; +cdb.core.sanitize = require('./core/sanitize'); +cdb.core.Template = require('./core/template'); +cdb.core.Model = require('./core/model'); +cdb.core.View = require('./core/view'); + +cdb.SQL = require('./api/sql'); + +cdb.createVis = require('./api/create-vis'); + +// log carto.js version +var logger = require('cdb.log'); +logger.log('carto.js ' + cdb.VERSION); + +cdb.helpers.GeoJSONHelper = require('./geo/geometry-models/geojson-helper'); + +module.exports = cdb; diff --git a/src/cdb.config.js b/src/cdb.config.js new file mode 100644 index 0000000..31e849c --- /dev/null +++ b/src/cdb.config.js @@ -0,0 +1,9 @@ +var Config = require('./core/config'); + +var config = new Config(); +config.set({ + cartodb_attributions: '© CARTO', + cartodb_logo_link: 'http://www.carto.com' +}); + +module.exports = config; diff --git a/src/cdb.js b/src/cdb.js new file mode 100644 index 0000000..81d8b9e --- /dev/null +++ b/src/cdb.js @@ -0,0 +1,10 @@ +// Creates cdb object, mutated in the entry file cartodb.js +// Used to avoid circular dependencies +var cdb = {}; + +cdb.VERSION = require('../package.json').version; +cdb.DEBUG = false; + +cdb.helpers = {}; + +module.exports = cdb; diff --git a/src/cdb.log.js b/src/cdb.log.js new file mode 100644 index 0000000..7daf8ed --- /dev/null +++ b/src/cdb.log.js @@ -0,0 +1,19 @@ +var cdb = require('cdb'); + +module.exports = { + error: function () { + console.error.apply(console, arguments); + }, + + log: function () { + console.log.apply(console, arguments); + }, + + info: function () { + console.log.apply(console, arguments); + }, + + debug: function () { + if (cdb.DEBUG) console.log.apply(console, arguments); + } +}; diff --git a/src/cdb.templates.js b/src/cdb.templates.js new file mode 100644 index 0000000..d4c5351 --- /dev/null +++ b/src/cdb.templates.js @@ -0,0 +1,3 @@ +var TemplateList = require('./core/template-list'); + +module.exports = new TemplateList(); diff --git a/src/constants.js b/src/constants.js new file mode 100644 index 0000000..51372d9 --- /dev/null +++ b/src/constants.js @@ -0,0 +1,28 @@ +module.exports = { + OVERLAY_TYPES: { + ATTRIBUTION: 'attribution', + HEADER: 'header', + LIMITS: 'limits', + TILES: 'tiles', + LOADER: 'loader', + LOGO: 'logo', + SEARCH: 'search', + ZOOM: 'zoom' + }, + + MAP_PROVIDER_TYPES: { + GMAPS: 'googlemaps', + LEAFLET: 'leaflet' + }, + + GMAPS_BASE_LAYER_TYPES: ['roadmap', 'gray_roadmap', 'dark_roadmap', 'hybrid', 'satellite', 'terrain'], + + WINDSHAFT_ERRORS: { + ANALYSIS: 'analysis', + LAYER: 'layer', + LIMIT: 'limit', + TILE: 'tile', // Generic error for tiles + GENERIC: 'generic', + UNKNOWN: 'unknown' + } +}; diff --git a/src/core/config.js b/src/core/config.js new file mode 100644 index 0000000..a641225 --- /dev/null +++ b/src/core/config.js @@ -0,0 +1,16 @@ +var Backbone = require('backbone'); + +/** + * global configuration + */ +var Config = Backbone.Model.extend({ + VERSION: 4, + + initialize: function () {}, + + // error track + REPORT_ERROR_URL: '/api/v0/error', + ERROR_TRACK_ENABLED: false +}); + +module.exports = Config; diff --git a/src/core/loader.js b/src/core/loader.js new file mode 100644 index 0000000..d022c56 --- /dev/null +++ b/src/core/loader.js @@ -0,0 +1,62 @@ +var Loader = { + queue: [], + current: undefined, + _script: null, + head: null, + + loadScript: function (src) { + var script = document.createElement('script'); + script.type = 'text/javascript'; + script.src = src; + script.async = true; + if (!Loader.head) { + Loader.head = document.getElementsByTagName('head')[0]; + } + // defer the loading because IE9 loads in the same frame the script + // so Loader._script is null + setTimeout(function () { + Loader.head.appendChild(script); + }, 0); + return script; + }, + + get: function (url, callback) { + if (!Loader._script) { + Loader.current = callback; + Loader._script = Loader.loadScript(url + (~url.indexOf('?') ? '&' : '?') + 'callback=vizjson'); + } else { + Loader.queue.push([url, callback]); + } + }, + + getPath: function (file) { + var scripts = document.getElementsByTagName('script'); + var cartodbJsRe = /\/?cartodb[\-\._]?([\w\-\._]*)\.js\??/; + for (var i = 0; i < scripts.length; i++) { + var src = scripts[i].src; + var matches = src.match(cartodbJsRe); + + if (matches) { + var bits = src.split('/'); + delete bits[bits.length - 1]; + return bits.join('/') + file; + } + } + return null; + } +}; + +// Required for jsonp callback, see Loader.get() +window.vizjson = function (data) { + Loader.current && Loader.current(data); + // remove script + Loader.head.removeChild(Loader._script); + Loader._script = null; + // next element + var a = Loader.queue.shift(); + if (a) { + Loader.get(a[0], a[1]); + } +}; + +module.exports = Loader; diff --git a/src/core/model.js b/src/core/model.js new file mode 100644 index 0000000..a28f2c1 --- /dev/null +++ b/src/core/model.js @@ -0,0 +1,81 @@ +var $ = require('jquery'); +var _ = require('underscore'); +var Backbone = require('backbone'); + +/** + * Base Model for all CartoDB model. + * DO NOT USE Backbone.Model directly + */ +var Model = Backbone.Model.extend({ + + initialize: function (options) { + _.bindAll(this, 'fetch', 'save', 'retrigger'); + return Backbone.Model.prototype.initialize.call(this, options); + }, + /** + * We are redefining fetch to be able to trigger an event when the ajax call ends, no matter if there's + * a change in the data or not. Why don't backbone does this by default? ahh, my friend, who knows. + * @method fetch + * @param args {Object} + */ + fetch: function (args) { + var self = this; + // var date = new Date(); + this.trigger('loadModelStarted'); + $.when(Backbone.Model.prototype.fetch.call(this, args)).done(function (ev) { + self.trigger('loadModelCompleted', ev, self); + // var dateComplete = new Date() + // console.log('completed in '+(dateComplete - date)); + }).fail(function (ev) { + self.trigger('loadModelFailed', ev, self); + }); + }, + /** + * Changes the attribute used as Id + * @method setIdAttribute + * @param attr {String} + */ + setIdAttribute: function (attr) { + this.idAttribute = attr; + }, + /** + * Listen for an event on another object and triggers on itself, with the same name or a new one + * @method retrigger + * @param ev {String} event who triggers the action + * @param obj {Object} object where the event happens + * @param obj {Object} [optional] name of the retriggered event + * @todo [xabel]: This method is repeated here and in the base view definition. There's should be a way to make it unique + */ + retrigger: function (ev, obj, retrigEvent) { + if (!retrigEvent) { + retrigEvent = ev; + } + var self = this; + obj.bind && obj.bind(ev, function () { + self.trigger(retrigEvent); + }, self); + }, + + /** + * We need to override backbone save method to be able to introduce new kind of triggers that + * for some reason are not present in the original library. Because you know, it would be nice + * to be able to differenciate "a model has been updated" of "a model is being saved". + * TODO: remove jquery from here + * @param {object} opt1 + * @param {object} opt2 + * @return {$.Deferred} + */ + save: function (opt1, opt2) { + var self = this; + if (!opt2 || !opt2.silent) this.trigger('saving'); + var promise = Backbone.Model.prototype.save.apply(this, arguments); + $.when(promise).done(function () { + if (!opt2 || !opt2.silent) self.trigger('saved'); + }).fail(function () { + if (!opt2 || !opt2.silent) self.trigger('errorSaving'); + }); + return promise; + } +}); + +module.exports = Model; diff --git a/src/core/profiler.js b/src/core/profiler.js new file mode 100644 index 0000000..a67e9d8 --- /dev/null +++ b/src/core/profiler.js @@ -0,0 +1,160 @@ +/* +# metrics profiler + +## timing + +``` + var timer = Profiler.metric('resource:load') + time.start(); + ... + time.end(); +``` + +## counters + +``` + var counter = Profiler.metric('requests') + counter.inc(); // 1 + counter.inc(10); // 11 + counter.dec() // 10 + counter.dec(10) // 0 +``` + +## Calls per second +``` + var fps = Profiler.metric('fps') + function render() { + fps.mark(); + } +``` +*/ +var MAX_HISTORY = 1024; +function Profiler () {} +Profiler.metrics = {}; +Profiler._backend = null; + +Profiler.get = function (name) { + return Profiler.metrics[name] || { + max: 0, + min: Number.MAX_VALUE, + avg: 0, + total: 0, + count: 0, + last: 0, + history: typeof (Float32Array) !== 'undefined' ? new Float32Array(MAX_HISTORY) : [] + }; +}; + +Profiler.backend = function (_) { + Profiler._backend = _; +}; + +Profiler.new_value = function (name, value, type, defer) { + type = type || 'i'; + var t = Profiler.metrics[name] = Profiler.get(name); + + t.max = Math.max(t.max, value); + t.min = Math.min(t.min, value); + t.total += value; + ++t.count; + t.avg = t.total / t.count; + t.history[t.count % MAX_HISTORY] = value; + + if (!defer) { + Profiler._backend && Profiler._backend([type, name, value]); + } else { + var n = performance.now(); + // don't allow to send stats quick + if (n - t.last > 1000) { + Profiler._backend && Profiler._backend([type, name, t.avg]); + t.last = n; + } + } +}; + +Profiler.print_stats = function () { + for (var k in Profiler.metrics) { + var t = Profiler.metrics[k]; + console.log(' === ' + k + ' === '); + console.log(' max: ' + t.max); + console.log(' min: ' + t.min); + console.log(' avg: ' + t.avg); + console.log(' count: ' + t.count); + console.log(' total: ' + t.total); + } +}; + +function Metric (name) { + this.t0 = null; + this.name = name; + this.count = 0; +} + +Metric.prototype = { + + // + // start a time measurement + // + start: function () { + this.t0 = performance.now(); + return this; + }, + + // elapsed time since start was called + _elapsed: function () { + return performance.now() - this.t0; + }, + + // + // finish a time measurement and register it + // ``start`` should be called first, if not this + // function does not take effect + // + end: function (defer) { + if (this.t0 !== null) { + Profiler.new_value(this.name, this._elapsed(), 't', defer); + this.t0 = null; + } + }, + + // + // increments the value + // qty: how many, default = 1 + // + inc: function (qty) { + qty = qty === undefined ? 1 : qty; + Profiler.new_value(this.name, qty, 'i'); + }, + + // + // decrements the value + // qty: how many, default = 1 + // + dec: function (qty) { + qty = qty === undefined ? 1 : qty; + Profiler.new_value(this.name, qty, 'd'); + }, + + // + // measures how many times per second this function is called + // + mark: function () { + ++this.count; + if (this.t0 === null) { + this.start(); + return; + } + var elapsed = this._elapsed(); + if (elapsed > 1) { + Profiler.new_value(this.name, this.count); + this.count = 0; + this.start(); + } + } +}; + +Profiler.metric = function (name) { + return new Metric(name); +}; + +module.exports = Profiler; diff --git a/src/core/sanitize.js b/src/core/sanitize.js new file mode 100644 index 0000000..ce1466e --- /dev/null +++ b/src/core/sanitize.js @@ -0,0 +1,24 @@ +var htmlCssSanitizer = require('html-css-sanitizer'); + +/** + * Sanitize inputHtml of unsafe HTML tags & attributes + * @param {String} inputHtml + * @param {Function} optionalSanitizer By default undefined, for which the default sanitizer will be used. + * Pass a function (that takes inputHtml) to sanitize yourself, or false/null to skip sanitize call. + */ +htmlCssSanitizer.html = function (inputHtml, optionalSanitizer) { + if (!inputHtml) return; + + if (optionalSanitizer === undefined) { + return htmlCssSanitizer.sanitize(inputHtml, function (url) { + // Return all URLs for (javascript: and data: URLs are removed prior to this fn is called) + return url; + }); + } else if (typeof optionalSanitizer === 'function') { + return optionalSanitizer(inputHtml); + } else { // alt sanitization set to false/null/other, treat as if caller takes responsibility to sanitize output + return inputHtml; + } +}; + +module.exports = htmlCssSanitizer; diff --git a/src/core/template-list.js b/src/core/template-list.js new file mode 100644 index 0000000..63a5b4c --- /dev/null +++ b/src/core/template-list.js @@ -0,0 +1,28 @@ +var _ = require('underscore'); +var Backbone = require('backbone'); +var log = require('cdb.log'); +var Template = require('./template'); + +var TemplateList = Backbone.Collection.extend({ + model: Template, + + getTemplate: function (templateName) { + if (this.namespace) { + templateName = this.namespace + templateName; + } + + var t = this.find(function (t) { + return t.get('name') === templateName; + }); + + if (t) { + return _.bind(t.render, t); + } + + log.error(templateName + ' not found'); + + return null; + } +}); + +module.exports = TemplateList; diff --git a/src/core/template.js b/src/core/template.js new file mode 100644 index 0000000..d9088d2 --- /dev/null +++ b/src/core/template.js @@ -0,0 +1,92 @@ +var _ = require('underscore'); +var Backbone = require('backbone'); +var Mustache = require('mustache'); +var log = require('cdb.log'); + +/** + * template system + * usage: + var tmpl = new Template({ + template: "hi, my name is {{ name }}", + type: 'mustache' // undescore by default + }); + console.log(tmpl.render({name: 'rambo'}))); + // prints "hi, my name is rambo" + + you could pass the compiled tempalte directly: + + var tmpl = new Template({ + compiled: function() { return 'my compiled template'; } + }); + */ +var Template = Backbone.Model.extend({ + + initialize: function () { + this.bind('change', this._invalidate); + this._invalidate(); + }, + + url: function () { + return this.get('template_url'); + }, + + parse: function (data) { + return { + 'template': data + }; + }, + + _invalidate: function () { + this.compiled = null; + if (this.get('template_url')) { + this.fetch(); + } + }, + + compile: function () { + var tmplType = this.get('type') || 'underscore'; + var fn = Template.compilers[tmplType]; + if (fn) { + return fn(this.get('template')); + } else { + log.error("can't get rendered for " + tmplType); + } + return null; + }, + + /** + * renders the template with specified vars + */ + render: function (vars) { + var c = this.compiled = this.compiled || this.get('compiled') || this.compile(); + var rendered = c(vars); + return rendered; + }, + + asFunction: function () { + return _.bind(this.render, this); + } + +}, { + compilers: { + 'underscore': _.template, + 'mustache': typeof (Mustache) === 'undefined' + ? null + // Replacement for Mustache.compile, which was removed in version 0.8.0 + : function compile (template) { + Mustache.parse(template); + return function (view, partials) { + return Mustache.render(template, view, partials); + }; + } + }, + compile: function (tmpl, type) { + var t = new Template({ + template: tmpl, + type: type || 'underscore' + }); + return _.bind(t.render, t); + } +}); + +module.exports = Template; diff --git a/src/core/util.js b/src/core/util.js new file mode 100644 index 0000000..6b7aec4 --- /dev/null +++ b/src/core/util.js @@ -0,0 +1,195 @@ +var _ = require('underscore'); + +var util = {}; + +util.isCORSSupported = function () { + return 'withCredentials' in new XMLHttpRequest(); +}; + +util.array2hex = function (byteArr) { + var encoded = []; + for (var i = 0; i < byteArr.length; ++i) { + encoded.push(String.fromCharCode(byteArr[i] + 128)); + } + return util.btoa(encoded.join('')); +}; + +util.btoa = function (data) { + if (typeof window['btoa'] === 'function') { + return util.encodeBase64Native(data); + } + + return util.encodeBase64(data); +}; + +util.encodeBase64Native = function (input) { + return btoa(input); +}; + +// ie7 btoa, +// from http://phpjs.org/functions/base64_encode/ +util.encodeBase64 = function (data) { + var b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; + var o1, o2, o3, h1, h2, h3, h4, bits; + var i = 0; + var ac = 0; + var enc = ''; + var tmpArr = []; + + if (!data) { + return data; + } + + do { + // pack three octets into four hexets + o1 = data.charCodeAt(i++); + o2 = data.charCodeAt(i++); + o3 = data.charCodeAt(i++); + + bits = (o1 << 16) | (o2 << 8) | o3; + + h1 = (bits >> 18) & 0x3f; + h2 = (bits >> 12) & 0x3f; + h3 = (bits >> 6) & 0x3f; + h4 = bits & 0x3f; + + // use hexets to index into b64, and append result to encoded string + tmpArr[ac++] = + b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4); + } while (i < data.length); + + enc = tmpArr.join(''); + + var r = data.length % 3; + return (r ? enc.slice(0, r - 3) : enc) + '==='.slice(r || 3); +}; + +util.uniqueCallbackName = function (str) { + util._callback_c = util._callback_c || 0; + ++util._callback_c; + return util.crc32(str) + '_' + util._callback_c; +}; + +util.crc32 = function (str) { + var crcTable = util._crcTable || (util._crcTable = util._makeCRCTable()); + var crc = 0 ^ -1; + + for (var i = 0, l = str.length; i < l; ++i) { + crc = (crc >>> 8) ^ crcTable[(crc ^ str.charCodeAt(i)) & 0xff]; + } + + return (crc ^ -1) >>> 0; +}; + +util._makeCRCTable = function () { + var c; + var crcTable = []; + for (var n = 0; n < 256; ++n) { + c = n; + for (var k = 0; k < 8; ++k) { + c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; + } + crcTable[n] = c; + } + return crcTable; +}; + +util._inferBrowser = function (ua) { + var browser = {}; + ua = + ua || (typeof window !== 'undefined' && window.navigator.userAgent) || ''; + function detectIE () { + var msie = ua.indexOf('MSIE '); + var trident = ua.indexOf('Trident/'); + if (msie > -1 || trident > -1) return true; + return false; + } + + function getIEVersion () { + if (!document.compatMode) return 5; + if (!window.XMLHttpRequest) return 6; + if (!document.querySelector) return 7; + if (!document.addEventListener) return 8; + if (!window.atob) return 9; + if (document.all) return 10; + else return 11; + } + + if (detectIE()) { + browser.ie = { version: getIEVersion() }; + } else if (ua.indexOf('Edge/') > -1) browser.edge = ua; + else if (ua.indexOf('Chrome') > -1) browser.chrome = ua; + else if (ua.indexOf('Firefox') > -1) browser.firefox = ua; + else if (ua.indexOf('Opera') > -1) browser.opera = ua; + else if (ua.indexOf('Safari') > -1) browser.safari = ua; + return browser; +}; + +util.browser = util._inferBrowser(); + +util.isMobileDevice = function () { + return /Android|webOS|iPhone|iPod|BlackBerry|IEMobile|Opera Mini/i.test( + navigator.userAgent + ); +}; + +util.supportsTouch = function () { + return 'ontouchstart' in window || navigator.msMaxTouchPoints; +}; + +var webGLSupportedAndEnabled = null; +util.isWebGLSupported = function () { + if (webGLSupportedAndEnabled === null) { + var canvas = document.createElement('canvas'); + webGLSupportedAndEnabled = + !!window.WebGLRenderingContext && + !!(canvas.getContext('webgl') || canvas.getContext('experimental-webgl')); + } + return webGLSupportedAndEnabled; +}; + +/** + * Returns true if the string ends with provided suffix + */ +util.endsWith = function (str, suffix) { + return str.indexOf(suffix, str.length - suffix.length) !== -1; +}; + +util.checkRequiredOpts = function (actualOpts, requiredOpts, from) { + _.each(requiredOpts, function (item) { + if (_.isUndefined(actualOpts[item])) { + throw new Error( + item + ' is required' + (from ? ' to initialize ' + from : '') + ); + } + }); +}; + +/** + * Checks that the correct Leaflet version is loaded + */ +util.isLeafletLoaded = function () { + if (!window.L) { + throw new Error('Leaflet is required'); + } + if (window.L.version < '1.0.0') { + throw new Error('Leaflet +1.0 is required'); + } +}; + +/** + * Checks that the correct Google Maps version is loaded + */ +util.isGoogleMapsLoaded = function () { + if (!window.google) { + throw new Error('Google Maps is required'); + } + if (!window.google.maps) { + throw new Error('Google Maps is required'); + } + if (window.google.maps.version < '3.31.0') { + throw new Error('Google Maps version should be >= 3.31'); + } +}; + +module.exports = util; diff --git a/src/core/view.js b/src/core/view.js new file mode 100644 index 0000000..9fcc07a --- /dev/null +++ b/src/core/view.js @@ -0,0 +1,173 @@ +var _ = require('underscore'); +var Backbone = require('backbone'); +var Profiler = require('cdb.core.Profiler'); +var templates = require('cdb.templates'); + +/** + * Base View for all CartoDB views. + * DO NOT USE Backbone.View directly + */ +var View = Backbone.View.extend({ + classLabel: 'cdb.core.View', + constructor: function (options) { + this.options = _.defaults(options, this.options); + this._models = []; + this._subviews = {}; + Backbone.View.call(this, options); + View.viewCount++; + View.views[this.cid] = this; + this._created_at = new Date(); + Profiler.new_value('total_views', View.viewCount); + }, + + add_related_model: function (m) { + if (!m) throw Error('added non valid model'); + this._models.push(m); + }, + + addView: function (v) { + this._subviews[v.cid] = v; + v._parent = this; + }, + + removeView: function (v) { + delete this._subviews[v.cid]; + }, + + clearSubViews: function () { + _(this._subviews).each(function (v) { + v.clean(); + }); + this._subviews = {}; + }, + + /** + * this method clean removes the view + * and clean and events associated. Call it when + * the view is not going to be used anymore + */ + clean: function () { + var self = this; + this.trigger('clean'); + this.clearSubViews(); + // remove from parent + if (this._parent) { + this._parent.removeView(this); + this._parent = null; + } + this.remove(); + this.unbind(); + this.stopListening(); + // remove this model binding + if (this.model && this.model.unbind) this.model.unbind(null, null, this); + // remove model binding + _(this._models).each(function (m) { + m.unbind(null, null, self); + }); + this._models = []; + View.viewCount--; + delete View.views[this.cid]; + return this; + }, + + /** + * utility methods + */ + + getTemplate: function (tmpl) { + if (this.options.template) { + return _.template(this.options.template); + } + return templates.getTemplate(tmpl); + }, + + show: function () { + this.$el.show(); + }, + + hide: function () { + this.$el.hide(); + }, + + /** + * Listen for an event on another object and triggers on itself, with the same name or a new one + * @method retrigger + * @param ev {String} event who triggers the action + * @param obj {Object} object where the event happens + * @param obj {Object} [optional] name of the retriggered event; + */ + retrigger: function (ev, obj, retrigEvent) { + if (!retrigEvent) { + retrigEvent = ev; + } + var self = this; + obj.bind && obj.bind(ev, function () { + self.trigger(retrigEvent); + }, self); + // add it as related model//object + this.add_related_model(obj); + }, + /** + * Captures an event and prevents the default behaviour and stops it from bubbling + * @method killEvent + * @param event {Event} + */ + killEvent: function (ev) { + if (ev && ev.preventDefault) { + ev.preventDefault(); + } + if (ev && ev.stopPropagation) { + ev.stopPropagation(); + } + }, + + /** + * Remove all the tipsy tooltips from the document + * @method cleanTooltips + */ + cleanTooltips: function () { + this.$('.tipsy').remove(); + } + +}, { + viewCount: 0, + views: {}, + + /** + * when a view with events is inherit and you want to add more events + * this helper can be used: + * var MyView = new core.View({ + * events: View.extendEvents({ + * 'click': 'fn' + * }) + * }); + */ + extendEvents: function (newEvents) { + return function () { + return _.extend(newEvents, this.constructor.__super__.events); + }; + }, + + /** + * search for views in a view and check if they are added as subviews + */ + runChecker: function () { + _.each(View.views, function (view) { + _.each(view, function (prop, k) { + if (k !== '_parent' && + view.hasOwnProperty(k) && + prop instanceof View && + view._subviews[prop.cid] === undefined) { + console.log('========='); + console.log('untracked view: '); + console.log(prop.el); + console.log('parent'); + console.log(view.el); + console.log(' '); + } + }); + }); + } +}); + +module.exports = View; diff --git a/src/dataviews/category-dataview-model.js b/src/dataviews/category-dataview-model.js new file mode 100644 index 0000000..55cda04 --- /dev/null +++ b/src/dataviews/category-dataview-model.js @@ -0,0 +1,320 @@ +var _ = require('underscore'); +var DataviewModelBase = require('./dataview-model-base'); +var SearchModel = require('./category-dataview/search-model'); +var CategoryModelRange = require('./category-dataview/category-model-range'); +var CategoriesCollection = require('./category-dataview/categories-collection'); + +/** + * Category dataview model + * + * - It has several internal models/collections + * - search model: it manages category search results. + * - filter model: it knows which items are accepted or rejected. + */ +module.exports = DataviewModelBase.extend({ + + defaults: _.extend( + { + type: 'category', + filterEnabled: false, + categories: 6, + allCategoryNames: [] // all (new + previously accepted), updated on data fetch (see parse) + }, + DataviewModelBase.prototype.defaults + ), + + _getDataviewSpecificURLParams: function () { + var params = [ + 'own_filter=' + (this.get('filterEnabled') ? 1 : 0), + 'categories=' + this.get('categories') + ]; + return params; + }, + + initialize: function (attrs, opts) { + DataviewModelBase.prototype.initialize.call(this, attrs, opts); + + // Internal model for calculating total amount of values in the category + this._rangeModel = new CategoryModelRange({ + apiKey: this._engine.getApiKey(), + authToken: this._engine.getAuthToken() + }); + + this._data = new CategoriesCollection(null, { + aggregationModel: this + }); + + this._searchModel = new SearchModel({ + apiKey: this._engine.getApiKey(), + authToken: this._engine.getAuthToken() + }, { + aggregationModel: this + }); + + this.on('change:column change:aggregation change:aggregation_column', this._reloadAndForceFetch, this); + this.on('change:categories', this.refresh, this); + + this.bind('change:url', function () { + this._searchModel.set({ + url: this.get('url') + }); + }, this); + + this.once('change:url', function () { + this._rangeModel.setUrl(this.get('url')); + }, this); + + this._rangeModel.bind('change:totalCount change:categoriesCount', function () { + this.set({ + totalCount: this._rangeModel.get('totalCount'), + categoriesCount: this._rangeModel.get('categoriesCount') + }); + }, this); + + this._bindSearchModelEvents(); + if (attrs && attrs.acceptedCategories) { + this.filter.accept(attrs.acceptedCategories); + } + }, + + _onMapBoundsChanged: function () { + DataviewModelBase.prototype._onMapBoundsChanged.apply(this, arguments); + this._searchModel.fetchIfSearchIsApplied(); + }, + + _onCircleChanged: function () { + DataviewModelBase.prototype._onCircleChanged.apply(this, arguments); + this._searchModel.fetchIfSearchIsApplied(); + }, + + _onPolygonChanged: function () { + DataviewModelBase.prototype._onPolygonChanged.apply(this, arguments); + this._searchModel.fetchIfSearchIsApplied(); + }, + + _bindSearchModelEvents: function () { + this.listenTo(this._searchModel, 'loading', function () { + this.trigger('loading', this); + }, this); + + this.listenTo(this._searchModel, 'loaded', function () { + this.trigger('loaded', this); + }, this); + + this.listenTo(this._searchModel, 'error', function (model, response) { + if (!response || (response && response.statusText !== 'abort')) { + this.trigger('error', model, response); + } + }, this); + + this.listenTo(this._searchModel, 'change:data', this._onSearchDataChange, this); + }, + + _onSearchDataChange: function () { + this.getSearchResult().each(function (m) { + var selected = this.filter.isAccepted(m.get('name')); + m.set('selected', selected); + }, this); + this.trigger('change:searchData', this); + }, + + _shouldFetchOnBoundingBoxChange: function () { + return DataviewModelBase.prototype._shouldFetchOnBoundingBoxChange.call(this) && !this.isSearchApplied(); + }, + + _shouldFetchOnCircleChange: function () { + return DataviewModelBase.prototype._shouldFetchOnCircleChange.call(this) && !this.isSearchApplied(); + }, + + _shouldFetchOnPolygonChange: function () { + return DataviewModelBase.prototype._shouldFetchOnPolygonChange.call(this) && !this.isSearchApplied(); + }, + + enableFilter: function () { + this.set('filterEnabled', true); + }, + + disableFilter: function () { + this.set('filterEnabled', false); + }, + + // Search model helper methods // + + getSearchQuery: function () { + return this._searchModel.getSearchQuery(); + }, + + setSearchQuery: function (q) { + this._searchModel.set('q', q); + }, + + isSearchValid: function () { + return this._searchModel.isValid(); + }, + + getSearchResult: function () { + return this._searchModel.getData(); + }, + + getSearchCount: function () { + return this._searchModel.getCount(); + }, + + applySearch: function () { + this._searchModel.fetch(); + }, + + isSearchApplied: function () { + return this._searchModel.isSearchApplied(); + }, + + cleanSearch: function () { + this._searchModel.resetData(); + }, + + setupSearch: function () { + if (!this.isSearchApplied()) { + this._searchModel.setData( + this._data.toJSON() + ); + } + }, + + getData: function () { + return this._data; + }, + + getSize: function () { + return this._data.size(); + }, + + getCount: function () { + return this.get('categoriesCount'); + }, + + isOtherAvailable: function () { + return this._data.isOtherAvailable(); + }, + + numberOfAcceptedCategories: function () { + var acceptedCategories = this.filter.acceptedCategories; + var numberOfRejectedCategories = this.numberOfRejectedCategories(); + var data = this.getData(); + var totalCategories = data.size(); + var numberOfAcceptedCategories = data.reduce( + function (memo, cat) { + var isCategoryInData = acceptedCategories.where({ name: cat.get('name') }).length > 0; + return memo + (isCategoryInData ? 1 : 0); + }, + 0 + ); + if (!numberOfRejectedCategories) { + return numberOfAcceptedCategories; + } else { + return totalCategories - numberOfRejectedCategories; + } + }, + + numberOfRejectedCategories: function () { + var rejectedCategories = this.filter.rejectedCategories; + var data = this.getData(); + return data.reduce( + function (memo, cat) { + var isCategoryInData = rejectedCategories.where({ name: cat.get('name') }).length > 0; + return memo + (isCategoryInData ? 1 : 0); + }, + 0 + ); + }, + + refresh: function () { + if (this.isSearchApplied()) { + this._searchModel.fetch(); + } else { + this.fetch(); + } + }, + + parse: function (d) { + var newData = []; + var _tmpArray = {}; + var allNewCategories = d.categories; + var allNewCategoryNames = []; + var acceptedCategoryNames = []; + + _.each(allNewCategories, function (datum) { + var category = datum.category; + + allNewCategoryNames.push(category); + var isRejected = this.filter.isRejected(category); + _tmpArray[category] = true; + + newData.push({ + selected: !isRejected, + name: category, + agg: datum.agg, + value: datum.value + }); + }, this); + + // Only accepted categories should appear when filterEnabled is true + if (this.get('filterEnabled')) { + // Add accepted items that are not present in the categories data + this.filter.acceptedCategories.each(function (mdl) { + var category = mdl.get('name'); + acceptedCategoryNames.push(category); + if (!_tmpArray[category]) { + newData.push({ + selected: true, + name: category, + agg: false, + value: 0 + }); + } + }, this); + } + + this._data.reset(newData); + + return { + allCategoryNames: _ + .chain(allNewCategoryNames) + .union(acceptedCategoryNames) + .unique() + .value(), + data: newData, + nulls: d.nulls, + min: d.min, + max: d.max, + count: d.count + }; + }, + + // Backbone toJson function override + // This function is used to serialize the server request + toJSON: function () { + return { + type: 'aggregation', + source: { id: this.getSourceId() }, + options: { + column: this.get('column'), + aggregation: this.get('aggregation'), + + // TODO server-side is using camelCased attr name, update once fixed + aggregationColumn: this.get('aggregation_column') + } + }; + } +}, + + // Class props +{ + ATTRS_NAMES: DataviewModelBase.ATTRS_NAMES.concat([ + 'column', + 'aggregation', + 'aggregation_column', + 'acceptedCategories', + 'categories' + ]) +} +); diff --git a/src/dataviews/category-dataview/categories-collection.js b/src/dataviews/category-dataview/categories-collection.js new file mode 100644 index 0000000..449c3ee --- /dev/null +++ b/src/dataviews/category-dataview/categories-collection.js @@ -0,0 +1,50 @@ +var Backbone = require('backbone'); +var _ = require('underscore'); +var CategoryItemModel = require('./category-item-model'); +var COUNT_AGGREGATION_TYPE = 'count'; + +/** + * Data categories collection + * + * - It basically sorts by (value, selected and "Other"). + */ + +module.exports = Backbone.Collection.extend({ + model: CategoryItemModel, + + initialize: function (models, options) { + this.aggregationModel = options.aggregationModel; + this.aggregation = options.aggregationModel.get('aggregation'); + }, + + reset: function (models, options) { + if (this.aggregationModel.get('aggregation') !== COUNT_AGGREGATION_TYPE) { + models = _.filter(models, function (category) { + var isModel = category instanceof Backbone.Model; + var value = isModel ? category.get('value') : category.value; + return value != null; + }); + } + Backbone.Collection.prototype.reset.call(this, models, options); + }, + + comparator: function (a, b) { + if (a.get('name') === 'Other') { + return 1; + } else if (b.get('name') === 'Other') { + return -1; + } else if (a.get('value') === b.get('value')) { + return (a.get('selected') < b.get('selected')) ? 1 : -1; + } else { + return (a.get('value') < b.get('value')) ? 1 : -1; + } + }, + + isOtherAvailable: function () { + return this.where({ + agg: true, + name: 'Other' + }).length > 0; + } + +}); diff --git a/src/dataviews/category-dataview/category-item-model.js b/src/dataviews/category-dataview/category-item-model.js new file mode 100644 index 0000000..effc648 --- /dev/null +++ b/src/dataviews/category-dataview/category-item-model.js @@ -0,0 +1,14 @@ +var Model = require('../../core/model'); + +/** + * Model for a category + */ +module.exports = Model.extend({ + + defaults: { + name: '', + agg: false, + value: 0 + } + +}); diff --git a/src/dataviews/category-dataview/category-model-range.js b/src/dataviews/category-dataview/category-model-range.js new file mode 100644 index 0000000..3dd32a7 --- /dev/null +++ b/src/dataviews/category-dataview/category-model-range.js @@ -0,0 +1,60 @@ +var _ = require('underscore'); +var Model = require('../../core/model'); + +/** + * This model is used for getting the total amount of values + * from the category. + * + */ + +module.exports = Model.extend({ + defaults: { + url: '', + totalCount: 0, + categoriesCount: 0 + }, + + url: function () { + var url = this.get('url'); + var queryOptions = []; + if (this.get('apiKey')) { + url += '?api_key=' + this.get('apiKey'); + } else if (this.get('authToken')) { + var authToken = this.get('authToken'); + if (authToken instanceof Array) { + _.each(authToken, function (token) { + queryOptions.push('auth_token[]=' + token); + }); + } else { + queryOptions.push('auth_token=' + authToken); + } + url += '?' + queryOptions.join('&'); + } + return url; + }, + + initialize: function () { + this.bind('change:url', function () { + this.fetch(); + }, this); + }, + + setUrl: function (url) { + this.set('url', url); + }, + + parse: function (d) { + // Calculating the total amount of all categories with the sum of all + // values from this model included the aggregated (Other) + return { + categoriesCount: d.categoriesCount, + totalCount: _.reduce( + _.pluck(d.categories, 'value'), + function (memo, value) { + return memo + value; + }, + 0 + ) + }; + } +}); diff --git a/src/dataviews/category-dataview/search-model.js b/src/dataviews/category-dataview/search-model.js new file mode 100644 index 0000000..ccbbc10 --- /dev/null +++ b/src/dataviews/category-dataview/search-model.js @@ -0,0 +1,127 @@ +var _ = require('underscore'); +var Model = require('../../core/model'); +var BackboneAbortSync = require('../../util/backbone-abort-sync'); +var CategoriesCollection = require('./categories-collection'); + +/** + * Category search model + */ +module.exports = Model.extend({ + defaults: { + q: '', + data: [], + url: '' + }, + + url: function () { + var url = this.get('url') + '/search?q=' + encodeURIComponent(this.get('q')); + if (this.get('apiKey')) { + url += '&api_key=' + this.get('apiKey'); + } else if (this.get('authToken')) { + var authToken = this.get('authToken'); + if (authToken instanceof Array) { + _.each(authToken, function (token) { + url += '&auth_token[]=' + token; + }); + } else { + url += '&auth_token=' + authToken; + } + } + return url; + }, + + initialize: function (attrs, opts) { + this._data = new CategoriesCollection(null, { + aggregationModel: opts.aggregationModel + }); + this.sync = BackboneAbortSync.bind(this); + }, + + fetchIfSearchIsApplied: function () { + if (this.isSearchApplied()) { + this.fetch(); + } + }, + + setData: function (data) { + var categories = this._parseData(data); + this._data.reset(categories); + this.set('data', categories); + }, + + getData: function () { + return this._data; + }, + + getSize: function () { + return this._data.size(); + }, + + getCount: function () { + return this.getSize(); + }, + + isValid: function () { + var str = this.get('q'); + return !!(str || ''); + }, + + resetData: function () { + this.setData([]); + this.set('q', ''); + }, + + getSearchQuery: function () { + return this.get('q'); + }, + + isSearchApplied: function () { + return this.isValid() && this.getSize() > 0; + }, + + _parseData: function (categories) { + var newData = []; + _.each(categories, function (d) { + if (!d.agg) { + newData.push({ + selected: false, + name: (d.category || d.name).toString(), + agg: d.agg, + value: d.value + }); + } + }, this); + + return newData; + }, + + parse: function (r) { + var categories = this._parseData(r.categories); + this._data.reset(categories); + return { + data: categories + }; + }, + + fetch: function (opts) { + opts = opts || {}; + + this.trigger('loading', this); + + if (opts.success) { + var successCallback = opts && opts.success; + } + + return Model.prototype.fetch.call(this, _.extend(opts, { + success: function () { + successCallback && successCallback(arguments); + this.trigger('loaded', this); + }.bind(this), + error: function (mdl, err) { + if (!err || (err && err.statusText !== 'abort')) { + this.trigger('error', mdl, err); + } + }.bind(this) + })); + } +}); diff --git a/src/dataviews/dataview-model-base.js b/src/dataviews/dataview-model-base.js new file mode 100644 index 0000000..4457d42 --- /dev/null +++ b/src/dataviews/dataview-model-base.js @@ -0,0 +1,572 @@ +var _ = require('underscore'); +var Model = require('../core/model'); +var BackboneAbortSync = require('../util/backbone-abort-sync'); +var AnalysisModel = require('../analysis/analysis-model'); +var util = require('../core/util'); +var parseWindshaftErrors = require('../windshaft/error-parser'); + +var UNFETCHED_STATUS = 'unfetched'; +var FETCHING_STATUS = 'fetching'; +var FETCHED_STATUS = 'fetched'; +var FETCH_ERROR_STATUS = 'error'; + +var REQUEST_GET_MAX_URL_LENGTH = 2083; // IE11 + +var REQUIRED_OPTS = [ + 'engine' +]; + +/** + * Default dataview model + */ +module.exports = Model.extend({ + defaults: { + url: '', + data: [], + sync_on_bbox_change: true, + sync_on_circle_change: true, + sync_on_polygon_change: true, + enabled: true, + status: UNFETCHED_STATUS + }, + + url: function () { + var params = _.union( + [ this._getSpatialFilterParam() ], + this._getDataviewSpecificURLParams() + ); + + this._addAuthTo(params); + + var urlWithParams = this.get('url') + '?' + params.join('&'); + + if (urlWithParams.length > REQUEST_GET_MAX_URL_LENGTH) { + throw new Error( + 'URL length is longer than allowed (' + REQUEST_GET_MAX_URL_LENGTH + ' chars). ' + + 'Check your filters (eg. if using a Polygon filter, reduce the number of vertices).' + ); + } + return urlWithParams; + }, + + _addAuthTo: function (params) { + if (this._engine.getApiKey()) { + params.push('api_key=' + this._engine.getApiKey()); + } else if (this._engine.getAuthToken()) { + var authToken = this._engine.getAuthToken(); + if (authToken instanceof Array) { + _.each(authToken, function (token) { + params.push('auth_token[]=' + token); + }); + } else { + params.push('auth_token=' + authToken); + } + } + }, + + _getSpatialFilterParam: function () { + if (this._bboxFilter) { + return this._getBoundingBoxFilterParam(); + } + + if (this._circleFilter) { + return this._getCircleFilterParam(); + } + + if (this._polygonFilter) { + return this._getPolygonFilterParam(); + } + }, + + _getBoundingBoxFilterParam: function () { + var result = ''; + + this._checkBBoxFilter(); + if (this.syncsOnBoundingBoxChanges()) { + result = 'bbox=' + this._bboxFilter.serialize(); + } + + return result; + }, + + _getCircleFilterParam: function () { + var result = ''; + + this._checkCircleFilter(); + if (this.syncsOnCircleChanges()) { + result = 'circle=' + this._circleFilter.serialize(); + } + + return result; + }, + + _getPolygonFilterParam: function () { + var result = ''; + + this._checkPolygonFilter(); + if (this.syncsOnPolygonChanges()) { + result = 'polygon=' + this._polygonFilter.serialize(); + } + + return result; + }, + + /** + * Subclasses might override this method to define extra params that will be appended + * to the dataview's URL. + * @return {Array} An array of strings in the form of "key=value". + */ + _getDataviewSpecificURLParams: function () { + return []; + }, + + initialize: function (attrs, opts) { + attrs = attrs || {}; + opts = opts || {}; + util.checkRequiredOpts(opts, REQUIRED_OPTS, 'DataviewModelBase'); + + this._hasBinds = false; + + this._engine = opts.engine; + + if (!attrs.source) throw new Error('source is a required attr'); + this._checkSourceAttribute(this.getSource()); + this.getSource().markAsSourceOf(this); + + if (!attrs.id) { + this.set('id', this.defaults.type + '-' + this.cid); + } + + this.sync = BackboneAbortSync.bind(this); + + // filter is optional, so have to guard before using it + this.filter = opts.filter; + if (this.filter) { + this.filter.set('dataviewId', this.id); + } + + this._addSpatialFilterFrom(opts); + + this._initBinds(); + }, + + _addSpatialFilterFrom (opts) { + if (opts.bboxFilter) { + this.addBBoxFilter(opts.bboxFilter); + } + + if (opts.circleFilter) { + this.addCircleFilter(opts.circleFilter); + } + + if (opts.polygonFilter) { + this.addPolygonFilter(opts.polygonFilter); + } + }, + + _initBinds: function () { + this.listenToOnce(this, 'change:url', function () { + this._checkBBoxFilter(); + if (this.syncsOnBoundingBoxChanges() && !this._bboxFilter.areBoundsAvailable()) { + // wait until map gets bounds from view + this.listenTo(this._bboxFilter, 'boundsChanged', this._fetch); + } else { + this._fetch(); + } + }); + + if (this.filter) { + this.listenTo(this.filter, 'change', this._onFilterChanged); + } + + this.getSource().on('change:status', this._onAnalysisStatusChange, this); + }, + + _onChangeBinds: function () { + this.on('change:sync_on_bbox_change', function () { + this.refresh(); + }, this); + + this.on('change:url', function (model, value, opts) { + this._newDataAvailable = true; + if (this._shouldFetchOnURLChange(opts && _.pick(opts, ['forceFetch', 'sourceId']))) { + this.refresh(); + } + }, this); + + this.on('change:enabled', function (mdl, isEnabled) { + if (isEnabled && this._newDataAvailable) { + this.refresh(); + this._newDataAvailable = false; + } + }, this); + }, + + _onMapBoundsChanged: function () { + if (this._shouldFetchOnBoundingBoxChange()) { + // If the widget is the first one created it changes the map bounds + // and cancels the first ._fetch request so we have to call ._fetch here + // instead of .refresh to set the binds if they're not set up yet + this._fetch(); + } + + if (this.syncsOnBoundingBoxChanges()) { + this._newDataAvailable = true; + } + }, + + _onCircleChanged: function () { + if (this._shouldFetchOnCircleChange()) { + this._fetch(); + } + + if (this.syncsOnCircleChanges()) { + this._newDataAvailable = true; + } + }, + + _onPolygonChanged: function () { + if (this._shouldFetchOnPolygonChange()) { + this._fetch(); + } + + if (this.syncsOnPolygonChanges()) { + this._newDataAvailable = true; + } + }, + + _fetch: function () { + this.fetch({ + success: function () { + if (!this._hasBinds) { + this._hasBinds = true; + this._onChangeBinds(); + } + }.bind(this) + }); + }, + + _onAnalysisStatusChange: function (analysis, status) { + if (analysis.isLoading()) { + this._triggerLoading(); + } else if (analysis.isFailed()) { + this._triggerStatusError(analysis.get('error')); + } + // loaded will be triggered through the default behavior, so not necessary to react on that status here + }, + + _triggerLoading: function () { + this.trigger('loading', this); + }, + + _triggerStatusError: function (error) { + this.trigger('statusError', this, error); // Backbone already emits an event `error` in failed requests. Avoiding name collision. + }, + + /** + * @protected + */ + _onFilterChanged: function (filter) { + this._reload({ + sourceId: this.getSourceId() + }); + }, + + _reloadAndForceFetch: function () { + this._reload({ + sourceId: this.getSourceId(), + forceFetch: true + }); + }, + + _reload: function (opts) { + opts = opts || {}; + this._engine.reload(opts); + }, + + _shouldFetchOnURLChange: function (options) { + options = options || {}; + var sourceId = options.sourceId; + var forceFetch = options.forceFetch; + + if (forceFetch) { + return true; + } + + return this.isEnabled() && + this._sourceAffectsMyOwnSource(sourceId); + }, + + _sourceAffectsMyOwnSource: function (sourceId) { + if (!sourceId) { + return true; + } + var sourceAnalysis = this.getSource(); + return sourceAnalysis && sourceAnalysis.findAnalysisById(sourceId); + }, + + _shouldFetchOnBoundingBoxChange: function () { + return this.isEnabled() && + this.syncsOnBoundingBoxChanges(); + }, + + _shouldFetchOnCircleChange: function () { + return this.isEnabled() && + this.syncsOnCircleChanges(); + }, + + _shouldFetchOnPolygonChange: function () { + return this.isEnabled() && + this.syncsOnPolygonChanges(); + }, + + refresh: function () { + this.fetch(); + }, + + addBBoxFilter: function (bboxFilter) { + if (!bboxFilter) { + return; + } + this._stopListeningBBoxChanges(); + this._bboxFilter = bboxFilter; + this._listenToBBoxChanges(); + }, + + removeBBoxFilter: function () { + this._stopListeningBBoxChanges(); + this._bboxFilter = null; + }, + + addCircleFilter: function (circleFilter) { + if (!circleFilter) { + return; + } + this._stopListeningCircleChanges(); + this._circleFilter = circleFilter; + this._listenToCircleChanges(); + }, + + removeCircleFilter: function () { + this._stopListeningCircleChanges(); + this._circleFilter = null; + }, + + addPolygonFilter: function (polygonFilter) { + if (!polygonFilter) { + return; + } + this._stopListeningPolygonChanges(); + this._polygonFilter = polygonFilter; + this._listenToPolygonChanges(); + }, + + removePolygonFilter: function () { + this._stopListeningPolygonChanges(); + this._polygonFilter = null; + }, + + update: function (attrs) { + if (_.has(attrs, 'source')) { + throw new Error('Source of dataviews cannot be updated'); + } + attrs = _.pick(attrs, this.constructor.ATTRS_NAMES); + + this.set(attrs); + }, + + getData: function () { + return this.get('data'); + }, + + getPreviousData: function () { + return this.previous('data'); + }, + + fetch: function (opts) { + opts = opts || {}; + this.set('status', FETCHING_STATUS); + + this._triggerLoading(); + + if (opts.success) { + var successCallback = opts && opts.success; + } + + return Model.prototype.fetch.call(this, _.extend(opts, { + success: function () { + this.set('status', FETCHED_STATUS); + successCallback && successCallback(arguments); + this.trigger('loaded', this); + }.bind(this), + error: function (_model, response) { + if (!response || (response && response.statusText !== 'abort')) { + this.set('status', FETCH_ERROR_STATUS); + if (this._errorWithBigPolygonFilter(_model)) { + response.statusText = 'error in the dataview request. ' + + 'Check the Polygon filter size (reduce the number of vertices and retry).'; + } + var error = this._parseError(response); + this._triggerStatusError(error); + } + }.bind(this) + })); + }, + + _errorWithBigPolygonFilter: function (model) { + var usingPolygonFilter = model && model.attributes && model.attributes.sync_on_polygon_change; + if (usingPolygonFilter && this.url().length > REQUEST_GET_MAX_URL_LENGTH) { + return true; + } + return false; + }, + + toJSON: function () { + throw new Error('toJSON should be defined for each dataview'); + }, + + getSourceType: function () { + return this.getSource().get('type'); + }, + + getSourceId: function () { + var source = this.getSource(); + return source && source.id; + }, + + getSource: function () { + return this.get('source'); + }, + + isSourceType: function () { + return this.getSourceType() === 'source'; + }, + + isFiltered: function () { + var isFiltered = false; + if (this.filter) { + isFiltered = !this.filter.isEmpty(); + } + return isFiltered; + }, + + remove: function () { + this._removeExistingAnalysisBindings(); + this.getSource().unmarkAsSourceOf(this); + this.trigger('destroy', this); + this.stopListening(); + }, + + _removeExistingAnalysisBindings: function () { + this.getSource().off('change:status', this._onAnalysisStatusChange, this); + }, + + isFetched: function () { + return this.get('status') === FETCHED_STATUS; + }, + + isUnavailable: function () { + return this.get('status') === FETCH_ERROR_STATUS; + }, + + isEnabled: function () { + return this.get('enabled'); + }, + + setUnavailable: function () { + return this.set('status', FETCH_ERROR_STATUS); + }, + + syncsOnBoundingBoxChanges: function () { + return this.get('sync_on_bbox_change'); + }, + + syncsOnCircleChanges: function () { + return this.get('sync_on_circle_change'); + }, + + syncsOnPolygonChanges: function () { + return this.get('sync_on_polygon_change'); + }, + + _checkSourceAttribute: function (source) { + if (!(source instanceof AnalysisModel)) { + throw new Error('Source must be an instance of AnalysisModel'); + } + }, + + _checkBBoxFilter: function () { + if (this.syncsOnBoundingBoxChanges() && !this._bboxFilter) { + throw new Error('Cannot sync on bounding box changes. There is no bounding box filter.'); + } + }, + + _checkCircleFilter: function () { + if (this.syncsOnCircleChanges() && !this._circleFilter) { + throw new Error('Cannot sync on circle filter changes. There is no circle filter.'); + } + }, + + _checkPolygonFilter: function () { + if (this.syncsOnPolygonChanges() && !this._polygonFilter) { + throw new Error('Cannot sync on polygon filter changes. There is no polygon filter.'); + } + }, + + _listenToBBoxChanges: function () { + if (this._bboxFilter) { + this.listenTo(this._bboxFilter, 'boundsChanged', this._onMapBoundsChanged); + } + }, + + _stopListeningBBoxChanges: function () { + if (this._bboxFilter) { + this.stopListening(this._bboxFilter, 'boundsChanged'); + } + }, + + _listenToCircleChanges: function () { + if (this._circleFilter) { + this.listenTo(this._circleFilter, 'circleChanged', this._onCircleChanged); + } + }, + + _stopListeningCircleChanges: function () { + if (this._circleFilter) { + this.stopListening(this._circleFilter, 'circleChanged'); + } + }, + + _listenToPolygonChanges: function () { + if (this._polygonFilter) { + this.listenTo(this._polygonFilter, 'polygonChanged', this._onPolygonChanged); + } + }, + + _stopListeningPolygonChanges: function () { + if (this._polygonFilter) { + this.stopListening(this._polygonFilter, 'polygonChanged'); + } + }, + + _parseError: function (response) { + var error = {}; + var errors = parseWindshaftErrors(response, 'dataview'); + if (errors.length > 0) { + error = errors[0]; + } + return error; + } +}, + +// Class props +{ + ATTRS_NAMES: [ + 'id', + 'sync_on_bbox_change', + 'sync_on_circle_change', + 'sync_on_polygon_change', + 'enabled', + 'source' + ] +}); diff --git a/src/dataviews/dataviews-collection.js b/src/dataviews/dataviews-collection.js new file mode 100644 index 0000000..6c65437 --- /dev/null +++ b/src/dataviews/dataviews-collection.js @@ -0,0 +1,24 @@ +var _ = require('underscore'); +var Backbone = require('backbone'); + +var DataviewsCollection = Backbone.Collection.extend({ + isAnyDataviewFiltered: function () { + return this.any(function (dataviewModel) { + var filter = dataviewModel.filter; + return (filter && !filter.isEmpty()); + }); + }, + + getFilters: function () { + return this.reduce(function (filters, dataviewModel) { + var filter = dataviewModel.filter; + if (filter && !filter.isEmpty()) { + filters['dataviews'] = filters['dataviews'] || {}; + _.extend(filters['dataviews'], filter.toJSON()); + } + return filters; + }, {}); + } +}); + +module.exports = DataviewsCollection; diff --git a/src/dataviews/dataviews-factory.js b/src/dataviews/dataviews-factory.js new file mode 100644 index 0000000..8bac7a2 --- /dev/null +++ b/src/dataviews/dataviews-factory.js @@ -0,0 +1,96 @@ +var _ = require('underscore'); +var Model = require('../core/model'); +var util = require('../core/util'); +var CategoryFilter = require('../windshaft/filters/category'); +var RangeFilter = require('../windshaft/filters/range'); +var CategoryDataviewModel = require('./category-dataview-model'); +var FormulaDataviewModel = require('./formula-dataview-model'); +var HistogramDataviewModel = require('./histogram-dataview-model'); +var BBoxFilter = require('../windshaft/filters/bounding-box'); +var MapModelBoundingBoxAdapter = require('../geo/adapters/map-model-bounding-box-adapter'); + +var REQUIRED_OPTS = [ + 'map', + 'engine', + 'dataviewsCollection' +]; + +/** + * Factory to create dataviews. + * Takes care of adding and wiring up lifeceycle to other related objects (e.g. dataviews collection, layers etc.) + */ +module.exports = Model.extend({ + + initialize: function (attrs, opts) { + util.checkRequiredOpts(opts, REQUIRED_OPTS, 'DataviewsFactory'); + + this._engine = opts.engine; + this._dataviewsCollection = opts.dataviewsCollection; + + var mapAdapter = new MapModelBoundingBoxAdapter(opts.map); + this._bboxFilter = new BBoxFilter(mapAdapter); + }, + + createCategoryModel: function (attrs) { + _checkProperties(attrs, ['source', 'column']); + attrs = this._generateAttrsForDataview(attrs, CategoryDataviewModel.ATTRS_NAMES); + attrs.aggregation = attrs.aggregation || 'count'; + attrs.aggregation_column = attrs.aggregation_column || attrs.column; + + var categoryFilter = new CategoryFilter(); + + return this._newModel( + new CategoryDataviewModel(attrs, { + engine: this._engine, + filter: categoryFilter, + bboxFilter: this._bboxFilter + }) + ); + }, + + createFormulaModel: function (attrs) { + _checkProperties(attrs, ['source', 'column', 'operation']); + attrs = this._generateAttrsForDataview(attrs, FormulaDataviewModel.ATTRS_NAMES); + var dataview = this._newModel( + new FormulaDataviewModel(attrs, { + engine: this._engine, + bboxFilter: this._bboxFilter + }) + ); + + return dataview; + }, + + createHistogramModel: function (attrs) { + _checkProperties(attrs, ['source', 'column']); + attrs = this._generateAttrsForDataview(attrs, HistogramDataviewModel.ATTRS_NAMES); + + var rangeFilter = new RangeFilter(); + + return this._newModel( + new HistogramDataviewModel(attrs, { + engine: this._engine, + filter: rangeFilter, + bboxFilter: this._bboxFilter + }) + ); + }, + + _generateAttrsForDataview: function (attrs, whitelistedAttrs) { + return _.pick(attrs, whitelistedAttrs); + }, + + _newModel: function (m) { + this._dataviewsCollection.add(m); + return m; + } + +}); + +function _checkProperties (obj, propertiesArray) { + _.each(propertiesArray, function (prop) { + if (obj[prop] === undefined) { + throw new Error(prop + ' is required'); + } + }); +} diff --git a/src/dataviews/formula-dataview-model.js b/src/dataviews/formula-dataview-model.js new file mode 100644 index 0000000..75e51f3 --- /dev/null +++ b/src/dataviews/formula-dataview-model.js @@ -0,0 +1,44 @@ +var _ = require('underscore'); +var DataviewModelBase = require('./dataview-model-base'); + +module.exports = DataviewModelBase.extend({ + defaults: _.extend( + { + type: 'formula', + data: '' + }, + DataviewModelBase.prototype.defaults + ), + + initialize: function () { + DataviewModelBase.prototype.initialize.apply(this, arguments); + this.on('change:column change:operation', this._reloadAndForceFetch, this); + }, + + parse: function (r) { + return { + data: r.result, + nulls: r.nulls + }; + }, + + toJSON: function () { + return { + type: 'formula', + source: { id: this.getSourceId() }, + options: { + column: this.get('column'), + operation: this.get('operation') + } + }; + } +}, + + // Class props +{ + ATTRS_NAMES: DataviewModelBase.ATTRS_NAMES.concat([ + 'column', + 'operation' + ]) +} +); diff --git a/src/dataviews/helpers/histogram-helper.js b/src/dataviews/helpers/histogram-helper.js new file mode 100644 index 0000000..78fa89c --- /dev/null +++ b/src/dataviews/helpers/histogram-helper.js @@ -0,0 +1,130 @@ +var _ = require('underscore'); + +var AGGREGATION_DATA = { + second: { unit: 'second', factor: 1 }, + minute: { unit: 'minute', factor: 1 }, + hour: { unit: 'hour', factor: 1 }, + day: { unit: 'day', factor: 1 }, + week: { unit: 'day', factor: 7 }, + month: { unit: 'month', factor: 1 }, + quarter: { unit: 'month', factor: 3 }, + year: { unit: 'month', factor: 12 }, + decade: { unit: 'month', factor: 120 }, + century: { unit: 'month', factor: 1200 }, + millennium: { unit: 'month', factor: 12000 } +}; + +var helper = {}; + +function trimBuckets (buckets, filledBuckets, totalBuckets) { + var index = null; + var keepGoing = true; + for (var i = filledBuckets.length - 1; i >= 0 && keepGoing && (_.isFinite(totalBuckets) ? i >= totalBuckets : true); i--) { + if (filledBuckets[i]) { + keepGoing = false; + } else { + index = i; + } + } + + return index !== null + ? buckets.slice(0, index) + : buckets; +} + +helper.fillTimestampBuckets = function (buckets, start, aggregation, numberOfBins, from, totalBuckets) { + var filledBuckets = []; // To catch empty buckets + var definedBucket = false; + + for (var i = 0; i < numberOfBins; i++) { + definedBucket = buckets[i] !== undefined; + filledBuckets.push(definedBucket); + + var bucketStart = this.add(start, i, aggregation); + var nextBucketStart = this.add(start, i + 1, aggregation); + + buckets[i] = _.extend({ + bin: i, + start: bucketStart, + end: nextBucketStart - 1, + next: nextBucketStart, + freq: 0 + }, buckets[i]); + delete buckets[i].timestamp; + } + + return from === 'totals' + ? buckets + : trimBuckets(buckets, filledBuckets, totalBuckets); +}; + +helper.fillNumericBuckets = function (buckets, start, width, numberOfBins) { + for (var i = 0; i < numberOfBins; i++) { + var bucketStart = start + (i * width); + var commonBucketEnd = start + ((i + 1) * width); + var isLastBucket = (i + 1) === numberOfBins; + var bucketEnd = (isLastBucket && buckets[i]) ? buckets[i].max : commonBucketEnd; + var filledBucket = _.extend({}, { + bin: i, + start: bucketStart, + end: bucketEnd, + freq: 0 + }, buckets[i]); + buckets[i] = filledBucket; + } +}; + +helper.hasChangedSomeOf = function (list, changed) { + return _.some(_.keys(changed), function (key) { + return _.contains(list, key); + }); +}; + +/** + * Add a `number` of aggregations to the provided timestamp + * + * @param {number} timestamp - Starting timestamp + * @param {number} number - Number of aggregations to add + * @param {object} aggregation + * @param {string} aggregation.unit - unit of the aggregation + * @param {number} aggregation.factor - number of aggretagion units + */ +helper.add = function (timestamp, number, aggregation) { + if (!AGGREGATION_DATA.hasOwnProperty(aggregation)) { + throw Error('aggregation "' + aggregation + '" is not defined'); + } + var date = new Date(timestamp * 1000); + var unit = AGGREGATION_DATA[aggregation].unit; + var factor = AGGREGATION_DATA[aggregation].factor; + var value = number * factor; + switch (unit) { + case 'second': + return date.setUTCSeconds(date.getUTCSeconds() + value) / 1000; + case 'minute': + return date.setUTCMinutes(date.getUTCMinutes() + value) / 1000; + case 'hour': + return date.setUTCHours(date.getUTCHours() + value) / 1000; + case 'day': + return date.setUTCDate(date.getUTCDate() + value) / 1000; + case 'month': + var n = date.getUTCDate(); + date.setUTCDate(1); + date.setUTCMonth(date.getUTCMonth() + value); + date.setUTCDate(Math.min(n, _getDaysInMonth(date.getUTCFullYear(), date.getUTCMonth()))); + return date.getTime() / 1000; + default: + return 0; + } +}; + +/* Internal functions */ + +function _getDaysInMonth (year, month) { + return [31, (_isLeapYear(year) ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month]; +} + +function _isLeapYear (year) { + return ((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0); +} + +module.exports = helper; diff --git a/src/dataviews/histogram-dataview-model.js b/src/dataviews/histogram-dataview-model.js new file mode 100644 index 0000000..2a0845c --- /dev/null +++ b/src/dataviews/histogram-dataview-model.js @@ -0,0 +1,444 @@ +var _ = require('underscore'); +var Backbone = require('backbone'); +var d3 = require('d3-array'); +var DataviewModelBase = require('./dataview-model-base'); +var HistogramDataModel = require('./histogram-dataview/histogram-data-model'); +var helper = require('./helpers/histogram-helper'); +var dateUtils = require('../util/date-utils'); + +module.exports = DataviewModelBase.extend({ + + defaults: _.extend( + { + type: 'histogram', + totalAmount: 0, + filteredAmount: 0, + hasNulls: false, + localTimezone: false + }, + DataviewModelBase.prototype.defaults + ), + + _getDataviewSpecificURLParams: function () { + var params = []; + + if (_.isNumber(this.get('own_filter'))) { + params.push('own_filter=' + this.get('own_filter')); + if (this.get('column_type') === 'number' && this.get('bins')) { + params.push('bins=' + this.get('bins')); + } + } else { + var offset = this.getCurrentOffset(); + + if (this.get('column_type') === 'number' && this.get('bins')) { + params.push('bins=' + this.get('bins')); + } else if (this.get('column_type') === 'date') { + params.push('aggregation=' + (this.get('aggregation') || 'auto')); + if (_.isFinite(offset)) { + params.push('offset=' + offset); + } + } + + // Start - End + var start = this.get('start'); + var end = this.get('end'); + if (_.isFinite(start) && _.isFinite(end)) { + params.push('start=' + start); + params.push('end=' + end); + } + } + return params; + }, + + initialize: function (attrs, opts) { + this._localOffset = dateUtils.getLocalOffset(); + + // Internal model for calculating all the data in the histogram (without filters) + this._totals = new HistogramDataModel({ + bins: this.get('bins'), + aggregation: this.get('aggregation'), + offset: this.get('offset'), + column_type: this.get('column_type'), + apiKey: opts && opts.engine && opts.engine.getApiKey(), + authToken: opts && opts.engine && opts.engine.getAuthToken(), + localTimezone: this.get('localTimezone'), + localOffset: this._localOffset, + start: this.get('start'), + end: this.get('end') + }); + + DataviewModelBase.prototype.initialize.apply(this, arguments); + this._data = new Backbone.Collection(this.get('data')); + + if (attrs && (attrs.min || attrs.max)) { + this.filter && this.filter.setRange(this.get('min'), this.get('max')); + } + }, + + _initBinds: function () { + DataviewModelBase.prototype._initBinds.apply(this); + + this._updateURLBinding(); + + // When original data gets fetched + this._totals.bind('loadModelCompleted', this._onTotalsDataFetched, this); + this._totals.once('loadModelCompleted', this._updateBindings, this); + this._totals.bind('error', this.setUnavailable, this); + this._totals.bind('error', this._onTotalsError, this); + + this.on('change:column', this._onColumnChanged, this); + this.on('change:localTimezone', this._onLocalTimezoneChanged, this); + this.on('change', this._onFieldsChanged, this); + this.on('change:column_type', this._onColumnTypeChanged, this); + }, + + _onLocalTimezoneChanged: function () { + this._totals.set('localTimezone', this.get('localTimezone')); + }, + + _updateURLBinding: function () { + this.off('change:url'); + this.on('change:url', this._onUrlChanged, this); + }, + + _updateBindings: function () { + this._onChangeBinds(); + this._updateURLBinding(); + }, + + enableFilter: function () { + this.set('own_filter', 1); + }, + + disableFilter: function () { + this.unset('own_filter'); + }, + + getData: function () { + return this._data.toJSON(); + }, + + getUnfilteredData: function () { + return this._totals.get('data'); + }, + + getUnfilteredDataModel: function () { + return this._totals; + }, + + getSize: function () { + return this._data.size(); + }, + + getColumnType: function () { + return this.get('column_type'); + }, + + hasNulls: function () { + return this.get('hasNulls'); + }, + + parse: function (data) { + var aggregation = data.aggregation || (this._totals && this._totals.get('aggregation')); + var numberOfBins = _.isFinite(data.bins_count) + ? data.bins_count + : this.get('bins'); + var width = data.bin_width; + var start = this.get('column_type') === 'date' ? data.timestamp_start : data.bins_start; + + var parsedData = { + data: [], + filteredAmount: 0, + nulls: 0, + totalAmount: 0 + }; + + if (this.has('error')) { + return parsedData; + } + + parsedData.data = new Array(numberOfBins); + + _.each(data.bins, function (bin) { + parsedData.data[bin.bin] = bin; + }); + + this.set({ + aggregation: aggregation + }, { silent: true }); + + if (this.get('column_type') === 'date') { + parsedData.data = helper.fillTimestampBuckets(parsedData.data, start, aggregation, numberOfBins, 'filtered', this._totals.get('data').length); + numberOfBins = parsedData.data.length; + } else { + helper.fillNumericBuckets(parsedData.data, start, width, numberOfBins); + } + + // if parse option is passed in the constructor, this._data is not created yet at this point + this._data && this._data.reset(parsedData.data); + + // Calculate totals + parsedData.totalAmount = this._calculateTotalAmount(parsedData.data); + parsedData.filteredAmount = this._calculateFilteredAmount(this.filter, this._data); + parsedData.nulls = data.nulls; + parsedData.bins = numberOfBins; + + if (data.nulls != null) { + parsedData = _.extend({}, parsedData, { + nulls: data.nulls, + hasNulls: true + }); + } + + return parsedData; + }, + + _onFilterChanged: function (filter) { + this.set('filteredAmount', this._calculateFilteredAmount(filter, this._data)); + + DataviewModelBase.prototype._onFilterChanged.apply(this, arguments); + }, + + _onColumnChanged: function () { + this._totals.set({ + column_type: this.get('column_type'), + column: this.get('column'), + start: null, + end: null + }); + this.set('aggregation', undefined, { silent: true }); + + this._reloadAndForceFetch(); + }, + + _calculateTotalAmount: function (buckets) { + return _.reduce(buckets, function (memo, bucket) { + var add = bucket && bucket.freq + ? bucket.freq + : 0; + return memo + add; + }, 0); + }, + + _calculateFilteredAmount: function (filter, data) { + var filteredAmount = 0; + if (filter && filter.get('min') !== void 0 && filter.get('max') !== void 0) { + var indexes = this._findBinsIndexes(data, filter.get('min'), filter.get('max')); + filteredAmount = this._sumBinsFreq(data, indexes.start, indexes.end); + } + + return filteredAmount; + }, + + _findBinsIndexes: function (data, start, end) { + var startBin = data.findWhere({ start: Math.min(start, end) }); + var endBin = data.findWhere({ end: Math.max(start, end) }); + + return { + start: startBin && startBin.get('bin'), + end: endBin && endBin.get('bin') + }; + }, + + _sumBinsFreq: function (data, start, end) { + return _.reduce(data.slice(start, end + 1), function (acum, d) { + return (d.get('freq') || 0) + acum; + }, 0); + }, + + /* + Ported from cartodb-postgresql + https://github.com/CartoDB/cartodb-postgresql/blob/master/scripts-available/CDB_DistType.sql + */ + getDistributionType: function (data) { + var histogram = data || this.get('data'); + var freqAccessor = function (a) { return a.freq; }; + var osc = d3.max(histogram, freqAccessor) - d3.min(histogram, freqAccessor); + var mean = d3.mean(histogram, freqAccessor); + // When the difference between the max and the min values is less than + // 10 percent of the mean, it's a flat histogram (F) + if (osc < mean * 0.1) return 'F'; + var sumFreqs = d3.sum(histogram, freqAccessor); + var freqs = histogram.map(function (bin) { + return 100 * bin.freq / sumFreqs; + }); + + // The ajus array represents relative growths + var ajus = freqs.map(function (freq, index) { + var next = freqs[index + 1]; + if (freq > next) return -1; + if (Math.abs(freq - next) <= 0.05) return 0; + return 1; + }); + ajus.pop(); + var maxAjus = d3.max(ajus); + var minAjus = d3.min(ajus); + // If it never grows or shrinks, it returns flat + if (minAjus === 0 && maxAjus === 0) return 'F'; + else if (maxAjus < 1) return 'L'; + else if (minAjus > -1) return 'J'; + else { + var uniques = _.uniq(ajus); + var A_TYPES = [[1, -1], [1, 0, -1], [1, -1, 0], [0, 1, -1]]; + var U_TYPES = [[-1, 1], [-1, 0, 1], [-1, 1, 0], [0, -1, 1]]; + if (A_TYPES.some(function (e) { + return _.isEqual(e, uniques); + })) return 'A'; + else if (U_TYPES.some(function (e) { + return _.isEqual(e, uniques); + })) return 'U'; + else return 'S'; + } + }, + + toJSON: function (d) { + var columnType = this.get('column_type'); + var offset = this.get('offset'); + + var options = { + column: this.get('column') + }; + + if (columnType === 'number' && this.get('bins')) { + options.bins = this.get('bins'); + } else if (columnType === 'date') { + options.aggregation = this.get('aggregation') || 'auto'; + + if (_.isFinite(offset)) { + options.offset = offset; + } + } + + return { + type: 'histogram', + source: { id: this.getSourceId() }, + options: options + }; + }, + + _onColumnTypeChanged: function () { + this.filter && this.filter.set('column_type', this.get('column_type')); + }, + + _onChangeBinds: function () { + DataviewModelBase.prototype._onChangeBinds.call(this); + }, + + _onUrlChanged: function () { + this._totals.set({ + offset: this.get('offset'), + bins: this.get('bins') + }, { silent: true }); + + this._totals.setUrl(this.get('url')); + }, + + _onTotalsDataFetched: function (data, model) { + var start = model.get('start'); + var end = model.get('end'); + + if (_.isFinite(start) && _.isFinite(end)) { + this.set({ + start: start, + end: end + }); + } + + this.set({ + aggregation: model.get('aggregation') || 'auto', + offset: model.get('offset') || 0, + bins: model.get('bins'), + error: model.get('error') + }, { silent: true }); + + var resetFilter = false; + + if (this.get('column_type') === 'date' && (_.has(this.changed, 'aggregation') || _.has(this.changed, 'offset'))) { + resetFilter = true; + } else if (this.get('column_type') === 'number' && _.has(this.changed, 'bins')) { + resetFilter = true; + } + + resetFilter + ? this._resetFilterAndFetch() + : this.fetch(); + }, + + _onFieldsChanged: function () { + this._setTotalsStartEnd(); + + if (!helper.hasChangedSomeOf(['bins', 'aggregation', 'offset'], this.changed)) { + return; + } + + var aggregationChangedToUndefined = _.has(this.changed, 'aggregation') && _.isUndefined(this.changed.aggregation); + + // We should avoid fetching totals when bins has changed and aggregation has + // changed to undefined. That means a change in column. If we set the bins + // we trigger a fetch while a map instantiation is ongoing. The API returns bad data in that case. + if (this.get('column_type') === 'number' && !aggregationChangedToUndefined) { + this._totals.set('bins', this.get('bins')); + } + if (this.get('column_type') === 'date') { + if (this.hasChanged('aggregation')) { + this._resetFilter(); + } + this._totals.set({ + offset: this.get('offset'), + aggregation: this.get('aggregation') + }); + } + }, + + _setTotalsStartEnd: function () { + const start = this.get('start'); + const end = this.get('end'); + + const startEndChanged = helper.hasChangedSomeOf(['start', 'end'], this.changed); + const startEndValid = _.isFinite(start) && _.isFinite(end); + const hasDifferentValues = this._totals.get('start') !== start || this._totals.get('end') !== end; + + if (startEndChanged && startEndValid && hasDifferentValues) { + this._totals.set({ start, end }); + this._totals.refresh(); + } + }, + + _resetFilterAndFetch: function () { + this._resetFilter(); + this.fetch(); + }, + + _resetFilter: function () { + this.disableFilter(); + this.filter && this.filter.unsetRange(); + }, + + _onTotalsError: function (model, error) { + var parsedError = error && this._parseError(error); + + if (parsedError && parsedError.message !== 'abort') { + this._triggerStatusError(parsedError); + } + }, + + getCurrentOffset: function () { + return this.get('localTimezone') + ? this._localOffset + : this.get('offset'); + } +}, + + // Class props +{ + ATTRS_NAMES: DataviewModelBase.ATTRS_NAMES.concat([ + 'column', + 'column_type', + 'bins', + 'min', + 'max', + 'aggregation', + 'offset' + ]) +} +); diff --git a/src/dataviews/histogram-dataview/histogram-data-model.js b/src/dataviews/histogram-dataview/histogram-data-model.js new file mode 100644 index 0000000..a0fa00f --- /dev/null +++ b/src/dataviews/histogram-dataview/histogram-data-model.js @@ -0,0 +1,155 @@ +var _ = require('underscore'); +var BackboneAbortSync = require('../../util/backbone-abort-sync'); +var Model = require('../../core/model'); +var helper = require('../helpers/histogram-helper'); + +/** + * This model is used for getting the total amount of data + * from the histogram widget (without any filter). + */ + +module.exports = Model.extend({ + defaults: { + url: '', + data: [], + localTimezone: false, + localOffset: 0, + hasBeenFetched: false + }, + + url: function () { + var params = []; + var columnType = this.get('column_type'); + var offset = this._getCurrentOffset(); + var aggregation = this.get('aggregation') || 'auto'; + + params.push('no_filters=1'); + + if (columnType === 'number' && this.get('bins')) { + params.push('bins=' + this.get('bins')); + } else if (columnType === 'date') { + params.push('aggregation=' + aggregation); + if (_.isFinite(offset)) { + params.push('offset=' + offset); + } + } + + if (this.get('apiKey')) { + params.push('api_key=' + this.get('apiKey')); + } else if (this.get('authToken')) { + var authToken = this.get('authToken'); + if (authToken instanceof Array) { + _.each(authToken, function (token) { + params.push('auth_token[]=' + token); + }); + } else { + params.push('auth_token=' + authToken); + } + } + + // Start - End + var start = this.get('start'); + var end = this.get('end'); + if (_.isFinite(start) && _.isFinite(end)) { + params.push('start=' + start); + params.push('end=' + end); + } + + return this.get('url') + '?' + params.join('&'); + }, + + initialize: function () { + this.sync = BackboneAbortSync.bind(this); + this._initBinds(); + }, + + _initBinds: function () { + this.on('change:url', function () { + this.refresh(); + }, this); + + this.on('change:aggregation change:offset', function () { + if (this.get('column_type') === 'date' && this.get('aggregation')) { + this.refresh(); + } + }, this); + + this.on('change:bins', function () { + if (this.get('column_type') === 'number') { + this.refresh(); + } + }, this); + + this.on('change:localTimezone', function () { + this.refresh(); + }, this); + + this.on('change:column', function () { + this.set('aggregation', 'auto', { silent: true }); + }); + + this.on('sync', function () { + this.set('hasBeenFetched', true); + }); + }, + + setUrl: function (url) { + if (!url) { + throw new Error('url not specified'); + } + this.set('url', url); + }, + + setBins: function (bins) { + this.set('bins', bins, { silent: bins === void 0 }); + }, + + getData: function () { + return this.get('data'); + }, + + parse: function (data) { + var aggregation = data.aggregation || this.get('aggregation'); + var numberOfBins = data.bins_count || 0; + var width = data.bin_width; + var start = this.get('column_type') === 'date' ? data.timestamp_start : data.bins_start; + + var parsedData = {}; + parsedData.data = new Array(numberOfBins); + + if (aggregation) { + parsedData.aggregation = aggregation; + this.set('aggregation', aggregation, { silent: true }); + } + + _.each(data.bins, function (bin) { + parsedData.data[bin.bin] = bin; + }); + + if (this.get('column_type') === 'date') { + parsedData.data = helper.fillTimestampBuckets(parsedData.data, start, aggregation, numberOfBins, 'totals'); + numberOfBins = parsedData.data.length; + } else { + helper.fillNumericBuckets(parsedData.data, start, width, numberOfBins); + } + + if (parsedData.data.length > 0) { + parsedData.start = parsedData.data[0].start; + parsedData.end = parsedData.data[parsedData.data.length - 1].end; + } + + parsedData.bins = numberOfBins; + + return parsedData; + }, + + refresh: function () { + this.fetch(); + }, + + _getCurrentOffset: function () { + return this.get('localTimezone') + ? this.get('localOffset') + : this.get('offset'); + } +}); diff --git a/src/engine.js b/src/engine.js new file mode 100644 index 0000000..7c1a8af --- /dev/null +++ b/src/engine.js @@ -0,0 +1,463 @@ +var _ = require('underscore'); +var AnalysisPoller = require('./analysis/analysis-poller'); +var AnonymousMapSerializer = require('./windshaft/map-serializer/anonymous-map-serializer/anonymous-map-serializer'); +var Backbone = require('backbone'); +var CartoDBLayerGroup = require('./geo/cartodb-layer-group'); +var DataviewsCollection = require('./dataviews/dataviews-collection'); +var LayersCollection = require('./geo/map/layers'); +var ModelUpdater = require('./windshaft-integration/model-updater'); +var NamedMapSerializer = require('./windshaft/map-serializer/named-map-serializer/named-map-serializer'); +var Request = require('./windshaft/request'); +var Response = require('./windshaft/response'); +var WindshaftClient = require('./windshaft/client'); +var AnalysisService = require('./analysis/analysis-service'); +var WindshaftError = require('./windshaft/error'); + +var RELOAD_DEBOUNCE_TIME_IN_MILIS = 100; + +/** + * + * Creates a new Engine. + * An engine is the core of a carto app. + * + * With the help of external services the engine will: + * + * - Keep the state of the layers and dataviews. + * - Serialize the state and send requests to the server. + * - Parse the server response and update the internal models. + * - Notify errors or successful operations. + * + * @param {Object} params - The parameters to initialize the engine. + * @param {string} params.apiKey - Api key used to be autenticate in the windshaft server. + * @param {string} params.authToken - Token used to be autenticate in the windshaft server. + * @param {string} params.username - Name of the user registered in the windshaft server. + * @param {string} params.serverUrl - Url of the windshaft server. + * @param {boolean} params.templateName - While we dont remove named maps we must explicitly say when the map is named. Defaults to false. + * @param {boolean} params.client - Token used to get map view statistics. + * @constructor + */ +function Engine (params) { + if (!params) throw new Error('new Engine() called with no parameters'); + this._isNamedMap = params.templateName !== undefined; + + // Variables for the reload debounce + this._timeout = null; + this._stackCalls = []; + this._batchOptions = {}; + + this._windshaftSettings = { + urlTemplate: params.serverUrl, + userName: params.username, + client: params.client, + apiKey: params.apiKey, + authToken: params.authToken, + templateName: params.templateName + }; + + this._windshaftClient = new WindshaftClient(this._windshaftSettings); + + // This object will be responsible of triggering the engine events. + this._eventEmmitter = _.extend({}, Backbone.Events); + + this._analysisPoller = new AnalysisPoller(); + this._layersCollection = new LayersCollection(); + this._dataviewsCollection = new DataviewsCollection(); + + this._cartoLayerGroup = new CartoDBLayerGroup( + { apiKey: params.apiKey, authToken: params.authToken }, + { layersCollection: this._layersCollection } + ); + this._bindCartoLayerGroupError(); + + this._modelUpdater = new ModelUpdater({ + dataviewsCollection: this._dataviewsCollection, + layerGroupModel: this._cartoLayerGroup, + layersCollection: this._layersCollection + }); +} + +/** + * Return the cartoLayergroup attached to the engine + */ +Engine.prototype.getLayerGroup = function () { + return this._cartoLayerGroup; +}; + +/** + * Returns the API key attached to the engine + */ +Engine.prototype.getApiKey = function () { + return this._windshaftSettings && this._windshaftSettings.apiKey; +}; + +/** + * Returns the Auth token attached to the engine + */ +Engine.prototype.getAuthToken = function () { + return this._windshaftSettings && this._windshaftSettings.authToken; +}; + +/** + * Bind a callback function to an event. The callback will be invoked whenever the event is fired. + * + * @param {string} event - The name of the event that triggers the callback execution. + * @param {function} callback - A function to be executed when the event is fired. + * @param {function} [context] - The context value for this when the callback is invoked. + * @example + * // Define a callback to be executed once the map is reloaded. + * function onReload(event) { + * console.log(event); // "reload-success" + * } + * // Attach the callback to the RELOAD_SUCCESS event. + * engine.on(Engine.Events.RELOAD_SUCCESS, onReload); + * // Call the reload method and wait. + * engine.reload(); + * + */ +Engine.prototype.on = function (event, callback, context) { + this._eventEmmitter.on(event, callback, context); +}; + +/** + * Remove a previously-bound callback function from an event. + * + * @param {string} event - The name of the event that triggers the callback execution. + * @param {function} callback - A function callback to be removed when the event is fired. + * @param {function} [context] - The context value for this when the callback is invoked. + * @example + * // Remove the the `displayMap` listener function so it wont be executed anymore when the engine fires the `load` event. + * engine.off(Engine.Events.RELOAD_SUCCESS, onReload); + * + */ +Engine.prototype.off = function (event, callback, context) { + this._eventEmmitter.off(event, callback, context); +}; + +/** + * This is the most important function of the engine. + * Generate a payload from the current state, send it to the windshaft server + * and update the internal models with the server response. + * + * Once the response has arrived trigger a 'reload-succes' or 'reload-error' event. + * + * @param {string} options.sourceId - The sourceId triggering the reload event. This is usefull to prevent uneeded requests and save data. + * @param {boolean} options.forceFetch - Forces dataviews to fetch data from server after a reload + * @param {boolean} options.includeFilters - Boolean flag to control if the filters need to be added in the payload. + * + * @fires Engine#Engine:RELOAD_STARTED + * @fires Engine#Engine:RELOAD_SUCCESS + * @fires Engine#Engine:RELOAD_ERROR + * + */ +Engine.prototype.reload = function (options) { + options = options || {}; + // Using a debouncer to optimize consecutive calls to reload the map. + // This allows to change multiple map parameters reloading the map only once, + // and therefore avoid the "You are over platform's limits" Windshaft error. + return new Promise(function (resolve, reject) { + this._batchOptions = _.pick({ + sourceId: options.sourceId, + forceFetch: this._batchOptions.forceFetch || options.forceFetch, + includeFilters: options.includeFilters + }, _.negate(_.isUndefined)); + this._stackCalls.push({ + success: options.success, + error: options.error, + resolve: resolve, + reject: reject + }); + var later = function () { + this._timeout = null; + this._performReload(this._batchOptions) + .then(function () { + // Resolve stacked callbacks and promises + this._stackCalls.forEach(function (call) { + call.success && call.success(); + call.resolve(); + }); + // Reset stack + this._stackCalls = []; + this._batchOptions = {}; + }.bind(this)) + .catch(function (windshaftError) { + // Reject stacked callbacks and promises + this._stackCalls.forEach(function (call) { + call.error && call.error(windshaftError); + call.reject(windshaftError); + }); + // Reset stack + this._stackCalls = []; + this._batchOptions = {}; + }.bind(this)); + }.bind(this); + clearTimeout(this._timeout); + this._timeout = setTimeout(later, RELOAD_DEBOUNCE_TIME_IN_MILIS); + }.bind(this)); +}; + +Engine.prototype._performReload = function (options) { + return new Promise(function (resolve, reject) { + // Build Windshaft options callbacks + var windshaftOptions = this._buildWindshaftOptions(options, + // Windshaft success callback + function (serverResponse) { + this._onReloadSuccess(serverResponse, options.sourceId, options.forceFetch); + resolve(); + }.bind(this), + // Windshaft error callback + function (errors) { + var windshaftError = this._onReloadError(errors); + reject(windshaftError); + }.bind(this) + ); + try { + var params = this._buildParams(windshaftOptions.includeFilters); + var payload = this._getSerializer().serialize(this._layersCollection, this._dataviewsCollection); + var request = new Request(payload, params, windshaftOptions); + + // Trigger STARTED event + this._eventEmmitter.trigger(Engine.Events.RELOAD_STARTED); + // Perform the request + this._windshaftClient.instantiateMap(request); + } catch (error) { + // Convert error in a windshaftError + var windshaftError = new WindshaftError({ message: error.message }); + this._manageClientError(windshaftError, windshaftOptions); + } + }.bind(this)); +}; + +/** + * + * Add a layer to the engine layersCollection + * + * @param {layer} layer - A new layer to be added to the engine. + * + * @public + */ +Engine.prototype.addLayer = function (layer) { + this._layersCollection.add(layer); +}; + +/** + * + * Remove a layer from the engine layersCollection + * + * @param {layer} layer - A new layer to be removed from the engine. + * + * @public + */ +Engine.prototype.removeLayer = function (layer) { + this._layersCollection.remove(layer); +}; + +/** + * + * Move a layer in the engine layersCollection + * + * @param {layer} layer - A new layer to be moved in the engine. + * @param {number} toIndex - Final index for the layer. + * + * @public + */ +Engine.prototype.moveLayer = function (layer, toIndex) { + var fromIndex = this._layersCollection.indexOf(layer); + if (fromIndex >= 0 && fromIndex !== toIndex) { + this._layersCollection.models.splice(toIndex, 0, this._layersCollection.models.splice(fromIndex, 1)[0]); + // Equivalent to: + // this._layersCollection.remove(layer, { silent: true }); + // this._layersCollection.add(layer, { at: toIndex }); + } +}; + +/** + * + * Add a dataview to the engine dataviewsCollection + * + * @param {Dataview} dataview - A new dataview to be added to the engine. + * + * @public + */ +Engine.prototype.addDataview = function (dataview) { + this._dataviewsCollection.add(dataview); +}; + +/** + * + * Remove a dataview from the engine dataviewsCollection + * + * @param {Dataview} dataview - The Dataview to be removed to the engine. + * + * @public + */ +Engine.prototype.removeDataview = function (dataview) { + this._dataviewsCollection.remove(dataview); +}; + +/** + * Callback executed when the windhsaft client returns a successful response. + * Update internal models and trigger a RELOAD_SUCCESS event. + * @private + */ +Engine.prototype._onReloadSuccess = function (serverResponse, sourceId, forceFetch) { + var responseWrapper = new Response(this._windshaftSettings, serverResponse); + this._modelUpdater.updateModels(responseWrapper, sourceId, forceFetch); + this._restartAnalysisPolling(); + // Trigger RELOAD_SUCCESS event + this._eventEmmitter.trigger(Engine.Events.RELOAD_SUCCESS); +}; + +/** + * Callback executed when the windhsaft client returns a failed response. + * Update internal models setting errors and trigger a RELOAD_ERROR event. + * @private + */ +Engine.prototype._onReloadError = function (errors) { + var windshaftError = this._getSimpleWindshaftError(errors); + this._modelUpdater.setErrors(errors); + // Trigger RELOAD_ERROR event + this._eventEmmitter.trigger(Engine.Events.RELOAD_ERROR, windshaftError); + return windshaftError; +}; + +/** + * Helper to get windhsaft request options. + * @private + */ +Engine.prototype._buildWindshaftOptions = function (options, successCallback, errorCallback) { + return _.extend({ + includeFilters: true, + success: successCallback, + error: errorCallback + }, _.pick(options, 'sourceId', 'forceFetch', 'includeFilters')); +}; + +/** + * Helper to get windhsaft request parameters. + * @param {boolean} includeFilters - Boolean flag to control if the filters need to be added in the payload. + * @private + */ +Engine.prototype._buildParams = function (includeFilters) { + var params = {}; + + if (__ENV__ === 'production') { + params.client = this._windshaftSettings.client; + } + + if (includeFilters && !_.isEmpty(this._dataviewsCollection.getFilters())) { + params.filters = this._dataviewsCollection.getFilters(); + } + + if (this._windshaftSettings.apiKey) { + params.api_key = this._windshaftSettings.apiKey; + return params; + } + + if (this._windshaftSettings.authToken) { + params.auth_token = this._windshaftSettings.authToken; + return params; + } + + console.warn('Engine initialized with no apiKeys neither authToken'); +}; + +/** + * Reset the analysis nodes in the poller + * @private + */ +Engine.prototype._restartAnalysisPolling = function () { + var analysisNodes = AnalysisService.getUniqueAnalysisNodes(this._layersCollection, this._dataviewsCollection); + this._analysisPoller.resetAnalysisNodes(analysisNodes); +}; + +/** + * Get the instance of the serializer service depending on is an anonymous or a named map. + * @private + */ +Engine.prototype._getSerializer = function () { + return this._isNamedMap ? NamedMapSerializer : AnonymousMapSerializer; +}; + +/** + * Manage and propagate the client error + * @private + */ +Engine.prototype._manageClientError = function (windshaftError, windshaftOptions) { + this._modelUpdater.setErrors([windshaftError]); + windshaftOptions.error && windshaftOptions.error([windshaftError]); +}; + +/** + * Listen to errors in cartoLayerGroup + */ +Engine.prototype._bindCartoLayerGroupError = function () { + this._cartoLayerGroup.on('all', function (change, error) { + if (change.lastIndexOf('error:', 0) === 0) { + error = new WindshaftError(error); + this._eventEmmitter.trigger(Engine.Events.LAYER_ERROR, error); + } + }, this); +}; + +Engine.prototype._getSimpleWindshaftError = function (errors) { + var error = _.find(errors, function (error) { return error.isGlobalError(); }); + if (!error && errors && errors.length > 0) { + error = errors[0]; + } + return error; +}; + +/** + * Events fired by the engine + * + * @readonly + * @enum {string} + */ +Engine.Events = { + /** + * Reload started event, fired every time the reload process starts. + */ + RELOAD_STARTED: 'reload-started', + /** + * Reload success event, fired every time the reload function succeed. + */ + RELOAD_SUCCESS: 'reload-success', + /** + * Reload error event, fired every time the reload function fails. + */ + RELOAD_ERROR: 'reload-error', + /** + * Error event, fired every time a tile or limit error happens. + */ + LAYER_ERROR: 'layer-error' +}; + +module.exports = Engine; + +/** + * Reload started event, fired every time the reload process starts. + * + * @event Engine#Engine:RELOAD_STARTED + * @type {string} + */ + +/** + * Reload success event, fired every time the reload function succeed. + * + * @event Engine#Engine:RELOAD_SUCCESS + * @type {string} + */ + +/** + * Reload success event, fired every time the reload function fails. + * + * @event Engine#Engine:RELOAD_ERROR + * @type {string} + */ + +/** + * Layer group error event, fired every time an error with layer group happends (tile or limit). + * + * @event Engine#Engine:LAYER_ERROR + * @type {string} + */ diff --git a/src/geo/adapters/gmaps-bounding-box-adapter.js b/src/geo/adapters/gmaps-bounding-box-adapter.js new file mode 100644 index 0000000..6f0d0c0 --- /dev/null +++ b/src/geo/adapters/gmaps-bounding-box-adapter.js @@ -0,0 +1,52 @@ +/* global google */ +var _ = require('underscore'); +var Model = require('../../core/model'); + +/** + * Adapt the Google Maps map to offer unique: + * - getBounds() function + * - 'boundsChanged' event + */ +module.exports = Model.extend({ + + initialize: function (map) { + this._isReady = false; + this._map = map; + this._debouncedTriggerBoundsChanged = _.debounce(this._triggerBoundsChanged, 200); + + google.maps.event.addListener( + this._map, + 'bounds_changed', + this._debouncedTriggerBoundsChanged.bind(this) + ); + }, + + getBounds: function () { + if (this._isReady) { + var mapBounds = this._map.getBounds(); + var sw = mapBounds.getSouthWest(); + var ne = mapBounds.getNorthEast(); + return { + west: sw.lng(), + south: sw.lat(), + east: ne.lng(), + north: ne.lat() + }; + } + return { + west: 0, + south: 0, + east: 0, + north: 0 + }; + }, + + clean: function () { + google.maps.event.clearListeners(this._map, 'bounds_changed'); + }, + + _triggerBoundsChanged: function () { + this._isReady = true; + this.trigger('boundsChanged', this.getBounds()); + } +}); diff --git a/src/geo/adapters/leaflet-bounding-box-adapter.js b/src/geo/adapters/leaflet-bounding-box-adapter.js new file mode 100644 index 0000000..d92131c --- /dev/null +++ b/src/geo/adapters/leaflet-bounding-box-adapter.js @@ -0,0 +1,40 @@ +var _ = require('underscore'); +var Model = require('../../core/model'); + +/** + * Adapt the Leaflet map to offer unique: + * - getBounds() function + * - 'boundsChanged' event + */ +module.exports = Model.extend({ + + initialize: function (map) { + this._map = map; + this._debouncedTriggerBoundsChanged = _.debounce(this._triggerBoundsChanged, 200); + this._map.on( + 'move zoom', + this._debouncedTriggerBoundsChanged, + this + ); + }, + + getBounds: function () { + var mapBounds = this._map.getBounds(); + var sw = mapBounds.getSouthWest(); + var ne = mapBounds.getNorthEast(); + return { + west: sw.lng, + south: sw.lat, + east: ne.lng, + north: ne.lat + }; + }, + + clean: function () { + this._map.off('move zoom'); + }, + + _triggerBoundsChanged: function () { + this.trigger('boundsChanged', this.getBounds()); + } +}); diff --git a/src/geo/adapters/map-model-bounding-box-adapter.js b/src/geo/adapters/map-model-bounding-box-adapter.js new file mode 100644 index 0000000..1c87837 --- /dev/null +++ b/src/geo/adapters/map-model-bounding-box-adapter.js @@ -0,0 +1,39 @@ +var _ = require('underscore'); +var Model = require('../../core/model'); + +/** + * Adapt the mapModel to offer unique: + * - getBounds() function + * - 'boundsChanged' event + */ + +module.exports = Model.extend({ + + initialize: function (map) { + this._map = map; + this._debouncedTriggerBoundsChanged = _.debounce(this._triggerBoundsChanged, 200); + this._map.on( + 'change:view_bounds_ne change:center change:zoom', + this._debouncedTriggerBoundsChanged, + this + ); + }, + + getBounds: function () { + var mapBounds = this._map.getViewBounds(); + return { + west: mapBounds[0][1], + south: mapBounds[0][0], + east: mapBounds[1][1], + north: mapBounds[1][0] + }; + }, + + clean: function () { + this._map.off('change:view_bounds_ne change:center change:zoom'); + }, + + _triggerBoundsChanged: function () { + this.trigger('boundsChanged', this.getBounds()); + } +}); diff --git a/src/geo/cartodb-layer-group-view-base.js b/src/geo/cartodb-layer-group-view-base.js new file mode 100644 index 0000000..d27696d --- /dev/null +++ b/src/geo/cartodb-layer-group-view-base.js @@ -0,0 +1,96 @@ +var parseWindshaftErrors = require('../windshaft/error-parser'); + +function CartoDBLayerGroupViewBase (layerGroupModel, opts) { + opts = opts || {}; + this.interaction = []; + this.nativeMap = opts.nativeMap; + this._mapModel = opts.mapModel; + + layerGroupModel.on('change:urls', this._reload, this); + layerGroupModel.onLayerVisibilityChanged(this._reload.bind(this)); + + this._reload(); +} + +CartoDBLayerGroupViewBase.prototype = { + _reload: function () { + throw new Error('_reload must be implemented'); + }, + + _reloadInteraction: function () { + this._clearInteraction(); + + this.model.forEachGroupedLayer(function (layerModel, layerIndex) { + if ((layerModel.isVisible()) && + (layerModel.isInteractive() || (this._mapModel && this._mapModel.isFeatureInteractivityEnabled()))) { + this._enableInteraction(layerIndex); + } + }, this); + }, + + _clearInteraction: function () { + for (var layerIndex in this.interaction) { + if (this.interaction.hasOwnProperty(layerIndex) && + this.interaction[layerIndex]) { + this.interaction[layerIndex].remove(); + this.interaction[layerIndex] = null; + } + } + }, + + _enableInteraction: function (layerIndexInLayerGroup) { + var self = this; + var tilejson = this._generateTileJSON(layerIndexInLayerGroup); + if (tilejson) { + var previousLayerInteraction = this.interaction[layerIndexInLayerGroup]; + if (previousLayerInteraction) { + previousLayerInteraction.remove(); + } + + // eslint-disable-next-line + this.interaction[layerIndexInLayerGroup] = new this.interactionClass() + .map(this.nativeMap) + .tilejson(tilejson) + .on('on', function (zeraEvent) { + if (self._interactionDisabled) return; + zeraEvent.layer = layerIndexInLayerGroup; + self._manageOnEvents(self.nativeMap, zeraEvent); + }) + .on('off', function (zeraEvent) { + if (self._interactionDisabled) return; + zeraEvent = zeraEvent || {}; + // TODO: zera has an .on('error', () => { }) callback that should be used here + if (zeraEvent.errors != null) { + self._manageInteractivityErrors(zeraEvent); + } + zeraEvent.layer = layerIndexInLayerGroup; + self._manageOffEvents(self.nativeMap, zeraEvent); + }); + } + }, + + _manageInteractivityErrors: function (payload) { + var errors = parseWindshaftErrors(payload); + if (errors.length > 0) { + this.trigger('featureError', errors[0]); + } + }, + + _generateTileJSON: function (layerIndexInLayerGroup) { + if (this.model.hasURLs()) { + return { + tilejson: '2.0.0', + scheme: 'xyz', + grids: this.model.getGridURLTemplatesWithSubdomains(layerIndexInLayerGroup), + tiles: this.model.getTileURLTemplatesWithSubdomains(), + formatter: function (options, data) { return data; } + }; + } + }, + + error: function (e) { }, + + tilesOk: function () { } +}; + +module.exports = CartoDBLayerGroupViewBase; diff --git a/src/geo/cartodb-layer-group.js b/src/geo/cartodb-layer-group.js new file mode 100644 index 0000000..d7734d0 --- /dev/null +++ b/src/geo/cartodb-layer-group.js @@ -0,0 +1,250 @@ +var _ = require('underscore'); +var $ = require('jquery'); +var Backbone = require('backbone'); +var LayerTypes = require('./map/layer-types'); +var util = require('../core/util'); + +var CartoDBLayerGroup = Backbone.Model.extend({ + defaults: { + visible: true, + type: 'layergroup' + }, + + initialize: function (attributes, options) { + options = options || {}; + + if (!options.layersCollection) { + throw new Error('layersCollection option is required'); + } + this._layersCollection = options.layersCollection; + }, + + addError: function (error) { + var type = error.type; + + if (!type) { + throw new Error('Error must have a type property.'); + } + + this.trigger('error:' + type, error); + }, + + forEachGroupedLayer: function (iteratee, context) { + _.each(this._getGroupedLayers(), iteratee.bind(context || this)); + }, + + _getGroupedLayers: function () { + return this._layersCollection.getCartoDBLayers(); + }, + + _getLayers: function () { + return this._layersCollection.reject(LayerTypes.isGoogleMapsBaseLayer); + }, + + getIndexOfLayerInLayerGroup: function (layerModel) { + return this._getGroupedLayers().indexOf(layerModel); + }, + + getLayerInLayerGroupAt: function (index) { + return this._getGroupedLayers()[index]; + }, + + getCartoLayerById: function (id) { + return this._layersCollection.get(id); + }, + + isEqual: function () { + return false; + }, + + hasURLs: function () { + return !!this.get('urls'); + }, + + getSubdomains: function () { + return (this.get('urls') && this.get('urls').subdomains) || []; + }, + + getTileURLTemplatesWithSubdomains: function () { + var urlTemplate = this.getTileURLTemplate(); + var subdomains = this.getSubdomains(); + + if (subdomains && subdomains.length) { + return _.map(subdomains, function (subdomain) { + return urlTemplate.replace('{s}', subdomain); + }); + } + + return [ urlTemplate ]; + }, + + getTileURLTemplate: function (type) { + type = type || 'png'; + + var tileURLTemplate = (this.get('urls') && this.get('urls').tiles); + + if (!tileURLTemplate) return ''; + + if (type === 'png') { + if (this._areAllLayersHidden()) { + return ''; + } + return this._generatePNGTileURLTemplate(tileURLTemplate); + } else if (type === 'mvt') { + return this._generateMTVTileURLTemplate(tileURLTemplate); + } + }, + + _generatePNGTileURLTemplate: function (urlTemplate) { + var mapnikLayersIndexes = this._getIndexesOfVisibleMapnikLayers(); + + if (mapnikLayersIndexes) { + urlTemplate = urlTemplate + .replace('{layerIndexes}', mapnikLayersIndexes) + .replace('{format}', 'png'); + + return this._appendAuthParamsToURL(urlTemplate); + } + + return ''; + }, + + _generateMTVTileURLTemplate: function (urlTemplate) { + urlTemplate = urlTemplate + .replace('{layerIndexes}', 'mapnik') + .replace('{format}', 'mvt'); + return this._appendAuthParamsToURL(urlTemplate); + }, + + _areAllLayersHidden: function () { + return _.all(this._getGroupedLayers(), function (layerModel) { + return !layerModel.isVisible(); + }); + }, + + _getIndexesOfVisibleMapnikLayers: function (url) { + var indexOfLayersInWindshaft = this.get('indexOfLayersInWindshaft'); + return _.reduce(this._getGroupedLayers(), function (indexes, layerModel, layerIndex) { + if (layerModel.isVisible()) { + indexes.push(indexOfLayersInWindshaft[layerIndex]); + } + return indexes; + }, []).join(','); + }, + + _getIndexesOfVisibleLayers: function (url) { + return _.reduce(this._getLayers(), function (indexes, layerModel, layerIndex) { + if (layerModel.isVisible()) { + indexes.push(layerIndex); + } + return indexes; + }, []).join(','); + }, + + hasTileURLTemplates: function () { + return !!this.getTileURLTemplate(); + }, + + getGridURLTemplatesWithSubdomains: function (layerIndex) { + var gridURLTemplates = (this.get('urls') && this.get('urls').grids && this.get('urls').grids[layerIndex]) || []; + + if (this.get('urls') && this.get('urls').subdomains) { + var subdomains = this.get('urls').subdomains; + gridURLTemplates = _.map(gridURLTemplates, function (url, i) { + return url.replace('{s}', subdomains[i]); + }); + } + + return _.map(gridURLTemplates, this._appendAuthParamsToURL, this); + }, + + getAttributesBaseURL: function (layerIndex) { + return this.get('urls') && this.get('urls').attributes && this.get('urls').attributes[layerIndex]; + }, + + getStaticImageURLTemplate: function () { + var staticImageURLTemplate = this.get('urls') && this.get('urls').image; + if (staticImageURLTemplate) { + staticImageURLTemplate = this._appendParamsToURL(staticImageURLTemplate, [ 'layer=' + this._getIndexesOfVisibleLayers() ]); + staticImageURLTemplate = this._appendAuthParamsToURL(staticImageURLTemplate); + staticImageURLTemplate = staticImageURLTemplate.replace('{s}', this.getSubdomains()[0]); + } + return staticImageURLTemplate; + }, + + fetchAttributes: function (layerIndex, featureID, callback) { + var attributeBaseURL = this.getAttributesBaseURL(layerIndex); + if (!attributeBaseURL) { + throw new Error('Attributes cannot be fetched until urls are set'); + } + + var url = this._appendAuthParamsToURL(attributeBaseURL + '/' + featureID); + + $.ajax({ + dataType: 'jsonp', + url: url, + jsonpCallback: '_cdbi_layer_attributes_' + util.uniqueCallbackName(this.toJSON()), + cache: true, + success: function (data) { + // loadingTime.end(); + callback(data); + }, + error: function (data) { + // loadingTime.end(); + // cartodb.core.Profiler.metric('cartodb-js.named_map.attributes.error').inc(); + callback(null); + } + }); + }, + + _appendAuthParamsToURL: function (url) { + var params = []; + if (this.get('apiKey')) { + params.push('api_key=' + this.get('apiKey')); + } else if (this.get('authToken')) { + var authToken = this.get('authToken'); + if (authToken instanceof Array) { + _.each(authToken, function (token) { + params.push('auth_token[]=' + token); + }); + } else { + params.push('auth_token=' + authToken); + } + } + + return this._appendParamsToURL(url, params); + }, + + _appendParamsToURL: function (url, params) { + if (params.length) { + var separator = '?'; + if (url.indexOf('?') !== -1) { + separator = '&'; + } + return url + separator + params.join('&'); + } + return url; + }, + + onLayerVisibilityChanged: function (callback) { + this._layersCollection.on('change:visible', function (layerModel) { + if (this._isLayerGrouped(layerModel)) { + callback(layerModel); + } + }, this); + }, + + onLayerAdded: function (callback) { + this._layersCollection.on('add', function (layerModel) { + if (this._isLayerGrouped(layerModel)) { + callback(layerModel, this.getLayerInLayerGroupAt(layerModel)); + } + }, this); + }, + + _isLayerGrouped: function (layerModel) { + return this._getGroupedLayers().indexOf(layerModel) >= 0; + } +}); + +module.exports = CartoDBLayerGroup; diff --git a/src/geo/geocoder/mapbox-geocoder.js b/src/geo/geocoder/mapbox-geocoder.js new file mode 100644 index 0000000..ead6862 --- /dev/null +++ b/src/geo/geocoder/mapbox-geocoder.js @@ -0,0 +1,81 @@ +var ENDPOINT = 'https://api.mapbox.com/geocoding/v5/mapbox.places-permanent/{{address}}.json?access_token={{access_token}}'; + +var TYPES = { + country: 'country', + region: 'region', + postcode: 'postal-area', + district: 'localadmin', + place: 'venue', + locality: 'locality', + neighborhood: 'neighbourhood', + address: 'address', + poi: 'venue', + 'poi.landmark': 'venue' +}; + +function MapboxGeocoder () { } + +MapboxGeocoder.geocode = function (address, token) { + if (!address) { + throw new Error('MapboxGeocoder.geocode called with no address'); + } + if (!token) { + throw new Error('MapboxGeocoder.geocode called with no access_token'); + } + return fetch(ENDPOINT.replace('{{address}}', address).replace('{{access_token}}', token)) + .then(function (response) { + return response.json(); + }) + .then(function (response) { + return _formatResponse(response); + }); +}; + +/** + * Transform a mapbox geocoder response on a object friendly with our search widget. + * @param {object} rawMapboxResponse - The raw mapbox geocoding response, {@see https://www.mapbox.com/api-documentation/?language=JavaScript#response-object} + */ +function _formatResponse (rawMapboxResponse) { + if (!rawMapboxResponse.features.length) { + return []; + } + return [{ + boundingbox: _getBoundingBox(rawMapboxResponse.features[0]), + center: _getCenter(rawMapboxResponse.features[0]), + type: _getType(rawMapboxResponse.features[0]) + }]; +} + +/** + * Mapbox returns [lon, lat] while we use [lat, lon] + */ +function _getCenter (feature) { + return [feature.center[1], feature.center[0]]; +} + +/** + * Transform the feature type into a well known enum. + */ +function _getType (feature) { + if (TYPES[feature.place_type[0]]) { + return TYPES[feature.place_type[0]]; + } + return 'default'; +} + +/** + * Transform the feature bbox into a carto.js well known format. + */ +function _getBoundingBox (feature) { + if (!feature.bbox) { + return; + } + return { + south: feature.bbox[0], + west: feature.bbox[1], + north: feature.bbox[2], + east: feature.bbox[3] + }; +} + +module.exports = MapboxGeocoder; diff --git a/src/geo/geocoder/tomtom-geocoder.js b/src/geo/geocoder/tomtom-geocoder.js new file mode 100644 index 0000000..013c20c --- /dev/null +++ b/src/geo/geocoder/tomtom-geocoder.js @@ -0,0 +1,95 @@ +var ENDPOINT = 'https://api.tomtom.com/search/2/search/{{address}}.json?key={{apiKey}}'; + +var TYPES = { + 'Geography': 'region', + 'Geography:Country': 'country', + 'Geography:CountrySubdivision': 'region', + 'Geography:CountrySecondarySubdivision': 'region', + 'Geography:CountryTertiarySubdivision': 'region', + 'Geography:Municipality': 'localadmin', + 'Geography:MunicipalitySubdivision': 'locality', + 'Geography:Neighbourhood': 'neighbourhood', + 'Geography:PostalCodeArea': 'postal-area', + 'Street': 'neighbourhood', + 'Address Range': 'neighbourhood', + 'Point Address': 'address', + 'Cross Street': 'address', + 'POI': 'venue' +}; + +function TomTomGeocoder () { } + +TomTomGeocoder.geocode = function (address, apiKey) { + if (!address) { + throw new Error('TomTomGeocoder.geocode called with no address'); + } + if (!apiKey) { + throw new Error('TomTomGeocoder.geocode called with no apiKey'); + } + return fetch(ENDPOINT.replace('{{address}}', address).replace('{{apiKey}}', apiKey)) + .then(function (response) { + return response.json(); + }) + .then(function (response) { + return _formatResponse(response); + }); +}; + +/** + * Transform a tomtom geocoder response into an object more friendly for our search widget. + * @param {object} rawTomTomResponse - The raw tomtom geocoding response, {@see https://developer.tomtom.com/search-api/search-api-documentation-geocoding/geocode} + */ +function _formatResponse (rawTomTomResponse) { + if (!rawTomTomResponse.results.length) { + return []; + } + + const bestCandidate = rawTomTomResponse.results[0]; + return [{ + boundingbox: _getBoundingBox(bestCandidate), + center: _getCenter(bestCandidate), + type: _getType(bestCandidate) + }]; +} + +/** + * TomTom returns { lon, lat } while we use [lat, lon] + */ +function _getCenter (result) { + return [result.position.lat, result.position.lon]; +} + +/** + * Transform the feature type into a well known enum. + */ +function _getType (result) { + let type = result.type; + if (TYPES[type]) { + if (type === 'Geography' && result.entityType) { + type = type + ':' + result.entityType; + } + return TYPES[type]; + } + + return 'default'; +} + +/** + * Transform the feature bbox into a carto.js well known format. + */ +function _getBoundingBox (result) { + if (!result.viewport) { + return; + } + const upperLeft = result.viewport.topLeftPoint; + const bottomRight = result.viewport.btmRightPoint; + + return { + south: bottomRight.lat, + west: upperLeft.lon, + north: upperLeft.lat, + east: bottomRight.lon + }; +} + +module.exports = TomTomGeocoder; diff --git a/src/geo/geometry-models/geojson-helper.js b/src/geo/geometry-models/geojson-helper.js new file mode 100644 index 0000000..ba0088f --- /dev/null +++ b/src/geo/geometry-models/geojson-helper.js @@ -0,0 +1,85 @@ +var _ = require('underscore'); + +var getGeometry = function (geoJSON) { + return geoJSON.geometry || geoJSON; +}; + +module.exports = { + getGeometryType: function (geoJSON) { + return getGeometry(geoJSON).type; + }, + + getGeometryCoordinates: function (geoJSON) { + return getGeometry(geoJSON).coordinates; + }, + + convertLatlngsToLnglats: function (latlngs) { + return _.map(latlngs, this.convertLatlngToLngLat); + }, + + convertLatlngToLngLat: function (latlng) { + return [latlng[1], latlng[0]]; + }, + + convertLngLatsToLatLngs: function (lnglats) { + return _.map(lnglats, this.convertLngLatToLatLng); + }, + + convertLngLatToLatLng: function (lnglat) { + return [ lnglat[1], lnglat[0] ]; + }, + + convertLatLngsToGeoJSONPointCoords: function (latlng) { + return this.convertLatlngToLngLat(latlng); + }, + + convertLatLngsToGeoJSONPolylineCoords: function (latlngs) { + return this.convertLatlngsToLnglats(latlngs); + }, + + convertLatLngsToGeoJSONPolygonCoords: function (latlngs) { + // Append the first latlng to "close" the polygon + latlngs = latlngs.concat([ latlngs[0] ]); + return this.convertLatlngsToLnglats(latlngs); + }, + + getPointLatLngFromGeoJSONCoords: function (geoJSON) { + var lnglat = this.getGeometryCoordinates(geoJSON); + return this.convertLngLatToLatLng(lnglat); + }, + + getPolylineLatLngsFromGeoJSONCoords: function (geoJSON) { + var lnglats = this.getGeometryCoordinates(geoJSON); + return this.convertLngLatsToLatLngs(lnglats); + }, + + getPolygonLatLngsFromGeoJSONCoords: function (geoJSON) { + var lnglats = this.getGeometryCoordinates(geoJSON)[0]; + var latlngs = this.convertLngLatsToLatLngs(lnglats); + // Remove the last latlng, which is duplicated + latlngs = latlngs.slice(0, -1); + return latlngs; + }, + + getMultiPointLatLngsFromGeoJSONCoords: function (geoJSON) { + var lnglats = this.getGeometryCoordinates(geoJSON); + return this.convertLngLatsToLatLngs(lnglats); + }, + + getMultiPolylineLatLngsFromGeoJSONCoords: function (geoJSON) { + var lnglats = this.getGeometryCoordinates(geoJSON); + return _.map(lnglats, function (lnglats) { + return this.convertLngLatsToLatLngs(lnglats); + }, this); + }, + + getMultiPolygonLatLngsFromGeoJSONCoords: function (geoJSON) { + var lnglats = this.getGeometryCoordinates(geoJSON); + return _.map(lnglats, function (lnglats) { + // Remove the last latlng, which is duplicated + var latlngs = this.convertLngLatsToLatLngs(lnglats[0]); + latlngs = latlngs.slice(0, -1); + return latlngs; + }, this); + } +}; diff --git a/src/geo/geometry-models/geometry-base.js b/src/geo/geometry-models/geometry-base.js new file mode 100644 index 0000000..ee36717 --- /dev/null +++ b/src/geo/geometry-models/geometry-base.js @@ -0,0 +1,45 @@ +var _ = require('underscore'); +var Model = require('../../core/model'); + +var GeometryBase = Model.extend({ + update: function () { + throw new Error('subclasses of GeometryBase must implement update'); + }, + + remove: function () { + this.trigger('remove'); + }, + + isComplete: function () { + throw new Error('subclasses of GeometryBase must implement isComplete'); + }, + + isEditable: function () { + return !!this.get('editable'); + }, + + isExpandable: function () { + return !!this.get('expandable'); + }, + + toGeoJSON: function () { + throw new Error('subclasses of GeometryBase must implement toGeoJSON'); + }, + + setCoordinatesFromGeoJSON: function (geoJSON) { + var coordinates = this.getCoordinatesFromGeoJSONCoords(geoJSON); + if (!_.isEqual(coordinates, this.getCoordinates())) { + this.setCoordinates(coordinates); + } + }, + + getCoordinatesFromGeoJSONCoords: function (geoJSON) { + throw new Error('subclasses of GeometryBase must implement getCoordinatesFromGeoJSONCoords'); + }, + + _triggerChangeEvent: function () { + this.trigger('change', this); + } +}); + +module.exports = GeometryBase; diff --git a/src/geo/geometry-models/geometry-factory.js b/src/geo/geometry-models/geometry-factory.js new file mode 100644 index 0000000..73204e0 --- /dev/null +++ b/src/geo/geometry-models/geometry-factory.js @@ -0,0 +1,91 @@ +var _ = require('underscore'); +var Point = require('./point'); +var Polyline = require('./polyline'); +var Polygon = require('./polygon'); +var MultiPoint = require('./multi-point'); +var MultiPolygon = require('./multi-polygon'); +var MultiPolyline = require('./multi-polyline'); +var GeoJSONHelper = require('./geojson-helper'); + +var createPoint = function (attrs, options) { + return new Point(attrs, options); +}; + +var createPolyline = function (attrs, options) { + return new Polyline(attrs, options); +}; + +var createPolygon = function (attrs, options) { + return new Polygon(attrs, options); +}; + +var createMultiPoint = function (attrs, options) { + return new MultiPoint(attrs, options); +}; + +var createMultiPolygon = function (attrs, options) { + return new MultiPolygon(attrs, options); +}; + +var createMultiPolyline = function (attrs, options) { + return new MultiPolyline(attrs, options); +}; + +var createPointFromGeoJSON = function (geoJSON, options) { + var latlng = GeoJSONHelper.getPointLatLngFromGeoJSONCoords(geoJSON); + return createPoint(_.extend({}, options, { latlng: latlng })); +}; + +var createPolylineFromGeoJSON = function (geoJSON, options) { + var latlngs = GeoJSONHelper.getPolylineLatLngsFromGeoJSONCoords(geoJSON); + return createPolyline(options, { latlngs: latlngs }); +}; + +var createPolygonFromGeoJSON = function (geoJSON, options) { + var latlngs = GeoJSONHelper.getPolygonLatLngsFromGeoJSONCoords(geoJSON); + return createPolygon(options, { latlngs: latlngs }); +}; + +var createMultiPointFromGeoJSON = function (geoJSON, options) { + var latlngs = GeoJSONHelper.getMultiPointLatLngsFromGeoJSONCoords(geoJSON); + return createMultiPoint(options, { latlngs: latlngs }); +}; + +var createMultiPolylineFromGeoJSON = function (geoJSON, options) { + var latlngs = GeoJSONHelper.getMultiPolylineLatLngsFromGeoJSONCoords(geoJSON); + return createMultiPolyline(options, { latlngs: latlngs }); +}; + +var createMultiPolygonFromGeoJSON = function (geoJSON, options) { + var latlngs = GeoJSONHelper.getMultiPolygonLatLngsFromGeoJSONCoords(geoJSON); + return createMultiPolygon(options, { latlngs: latlngs }); +}; + +var GEOJSON_TYPE_TO_CREATE_METHOD = { + Point: createPointFromGeoJSON, + LineString: createPolylineFromGeoJSON, + Polygon: createPolygonFromGeoJSON, + MultiPoint: createMultiPointFromGeoJSON, + MultiPolygon: createMultiPolygonFromGeoJSON, + MultiLineString: createMultiPolylineFromGeoJSON +}; + +var createGeometryFromGeoJSON = function (geoJSON, options) { + var geometryType = GeoJSONHelper.getGeometryType(geoJSON); + var createMethod = GEOJSON_TYPE_TO_CREATE_METHOD[geometryType]; + if (createMethod) { + return createMethod(geoJSON, options); + } + + throw new Error('Geometries of type ' + geometryType + ' are not supported yet'); +}; + +module.exports = { + createPoint: createPoint, + createPolyline: createPolyline, + createPolygon: createPolygon, + createMultiPoint: createMultiPoint, + createMultiPolyline: createMultiPolyline, + createMultiPolygon: createMultiPolygon, + createGeometryFromGeoJSON: createGeometryFromGeoJSON +}; diff --git a/src/geo/geometry-models/multi-geometry-base.js b/src/geo/geometry-models/multi-geometry-base.js new file mode 100644 index 0000000..b3b4e1b --- /dev/null +++ b/src/geo/geometry-models/multi-geometry-base.js @@ -0,0 +1,52 @@ +var _ = require('underscore'); +var Backbone = require('backbone'); +var GeometryBase = require('./geometry-base'); + +var MultiGeometryBase = GeometryBase.extend({ + initialize: function (attrs, options) { + GeometryBase.prototype.initialize.apply(this, arguments); + options = options || {}; + + this.geometries = new Backbone.Collection(); + if (options.latlngs) { + this.geometries.reset(this._createGeometries(options.latlngs)); + } + this.geometries.on('change', this._triggerChangeEvent, this); + this.geometries.on('reset', this._onGeometriesReset, this); + }, + + _createGeometries: function (latlngs) { + return _.map(latlngs, this._createGeometry, this); + }, + + _createGeometry: function (latlngs) { + throw new Error('subclasses of MultiGeometryBase must implement _createGeometry'); + }, + + update: function (latlng) {}, + + remove: function () { + GeometryBase.prototype.remove.apply(this); + this._removeGeometries(); + }, + + isComplete: function () { + return this.geometries.all(function (geometry) { + return geometry.isComplete(); + }); + }, + + _onGeometriesReset: function (collection, options) { + this._removeGeometries(options.previousModels); + this._triggerChangeEvent(); + }, + + _removeGeometries: function (geometries) { + geometries = geometries || this.geometries.models; + _.each(geometries, function (geometry) { + geometry.remove(); + }, this); + } +}); + +module.exports = MultiGeometryBase; diff --git a/src/geo/geometry-models/multi-point.js b/src/geo/geometry-models/multi-point.js new file mode 100644 index 0000000..09b0a0d --- /dev/null +++ b/src/geo/geometry-models/multi-point.js @@ -0,0 +1,37 @@ +var Point = require('./point'); +var GeoJSONHelper = require('./geojson-helper'); +var MultiGeometryBase = require('./multi-geometry-base'); + +var MultiPoint = MultiGeometryBase.extend({ + defaults: { + editable: false + }, + + _createGeometry: function (latlng) { + return new Point({ + latlng: latlng, + editable: this.isEditable() + }); + }, + + toGeoJSON: function () { + var coords = this.geometries.map(function (path) { + return GeoJSONHelper.convertLatLngsToGeoJSONPointCoords(path.getCoordinates()); + }); + return { + 'type': 'MultiPoint', + 'coordinates': coords + }; + }, + + setCoordinatesFromGeoJSON: function (geoJSON) { + var latlngs = this.getCoordinatesFromGeoJSONCoords(geoJSON); + this.geometries.reset(this._createGeometries(latlngs)); + }, + + getCoordinatesFromGeoJSONCoords: function (geoJSON) { + return GeoJSONHelper.getMultiPointLatLngsFromGeoJSONCoords(geoJSON); + } +}); + +module.exports = MultiPoint; diff --git a/src/geo/geometry-models/multi-polygon.js b/src/geo/geometry-models/multi-polygon.js new file mode 100644 index 0000000..5ed4006 --- /dev/null +++ b/src/geo/geometry-models/multi-polygon.js @@ -0,0 +1,40 @@ +var Polygon = require('./polygon'); +var GeoJSONHelper = require('./geojson-helper'); +var MultiGeometryBase = require('./multi-geometry-base'); + +var MultiPolygon = MultiGeometryBase.extend({ + defaults: { + editable: false, + expandable: false + }, + + _createGeometry: function (latlngs) { + return new Polygon({ + editable: this.isEditable(), + expandable: this.isExpandable() + }, { + latlngs: latlngs + }); + }, + + toGeoJSON: function () { + var coords = this.geometries.map(function (path) { + return [ GeoJSONHelper.convertLatLngsToGeoJSONPolygonCoords(path.getCoordinates()) ]; + }); + return { + 'type': 'MultiPolygon', + 'coordinates': coords + }; + }, + + setCoordinatesFromGeoJSON: function (geoJSON) { + var latlngs = this.getCoordinatesFromGeoJSONCoords(geoJSON); + this.geometries.reset(this._createGeometries(latlngs)); + }, + + getCoordinatesFromGeoJSONCoords: function (geoJSON) { + return GeoJSONHelper.getMultiPolygonLatLngsFromGeoJSONCoords(geoJSON); + } +}); + +module.exports = MultiPolygon; diff --git a/src/geo/geometry-models/multi-polyline.js b/src/geo/geometry-models/multi-polyline.js new file mode 100644 index 0000000..c4473af --- /dev/null +++ b/src/geo/geometry-models/multi-polyline.js @@ -0,0 +1,40 @@ +var Polyline = require('./polyline'); +var GeoJSONHelper = require('./geojson-helper'); +var MultiGeometryBase = require('./multi-geometry-base'); + +var MultiPolyline = MultiGeometryBase.extend({ + defaults: { + editable: false, + expandable: false + }, + + _createGeometry: function (latlngs) { + return new Polyline({ + editable: this.isEditable(), + expandable: this.isExpandable() + }, { + latlngs: latlngs + }); + }, + + toGeoJSON: function () { + var coords = this.geometries.map(function (path) { + return GeoJSONHelper.convertLatLngsToGeoJSONPolylineCoords(path.getCoordinates()); + }); + return { + 'type': 'MultiLineString', + 'coordinates': coords + }; + }, + + setCoordinatesFromGeoJSON: function (geoJSON) { + var latlngs = this.getCoordinatesFromGeoJSONCoords(geoJSON); + this.geometries.reset(this._createGeometries(latlngs)); + }, + + getCoordinatesFromGeoJSONCoords: function (geoJSON) { + return GeoJSONHelper.getMultiPolylineLatLngsFromGeoJSONCoords(geoJSON); + } +}); + +module.exports = MultiPolyline; diff --git a/src/geo/geometry-models/path-base.js b/src/geo/geometry-models/path-base.js new file mode 100644 index 0000000..87c9ae6 --- /dev/null +++ b/src/geo/geometry-models/path-base.js @@ -0,0 +1,75 @@ +var _ = require('underscore'); +var Backbone = require('backbone'); +var Point = require('./point'); +var GeometryBase = require('./geometry-base'); + +var PathBase = GeometryBase.extend({ + initialize: function (attrs, options) { + GeometryBase.prototype.initialize.apply(this, arguments); + options = options || {}; + + this.points = new Backbone.Collection(); + + if (options.latlngs) { + this.points.reset(this._createPoints(options.latlngs)); + } + + this.points.on('change', this._triggerChangeEvent, this); + this.points.on('remove', this._triggerChangeEvent, this); + }, + + getCoordinates: function () { + return this.points.map(function (point) { + return point.getCoordinates(); + }); + }, + + setCoordinates: function (latlngs) { + this.points.reset(this._createPoints(latlngs)); + this._triggerChangeEvent(); + }, + + getCoordinatesForMiddlePoints: function () { + return this.getCoordinates(); + }, + + update: function (latlng) { + var latlngs = this.getCoordinates(); + latlngs.push(latlng); + this.setCoordinates(latlngs); + }, + + _createPoints: function (latlngs) { + return _.map(latlngs, this._createPoint, this); + }, + + _createPoint: function (latlng) { + var point = new Point({ + latlng: latlng, + editable: this.isEditable() + }); + + point.once('dblclick', function (event) { + this.removePoint(point); + }, this); + + return point; + }, + + addPoint: function (point, options) { + options = options || {}; + var at = options.at || 0; + + point.once('dblclick', function () { + this.removePoint(point); + }, this); + + this.points.add(point, { at: at }); + }, + + removePoint: function (point) { + this.points.remove(point); + } +}); + +module.exports = PathBase; diff --git a/src/geo/geometry-models/point.js b/src/geo/geometry-models/point.js new file mode 100644 index 0000000..c77b0c6 --- /dev/null +++ b/src/geo/geometry-models/point.js @@ -0,0 +1,49 @@ +var GeoJSONHelper = require('./geojson-helper'); +var GeometryBase = require('./geometry-base'); + +var DEFAULT_ICON_URL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAWBAMAAAA2mnEIAAAAKlBMVEUAAAD////////////////////////////////e6fPO3+5zo85Cg705fbrPhX3RAAAADXRSTlMAADVBRn6fqLO9weP7Sr4MvgAAAHRJREFUGNNjEEQABhQ2AwNLeEepAwOYzZTR0dHRpgBmW3SAQDOIzdTRuefu6RkdikA2W8eau3fvnupIBLI9Os8C2XdmtADZEd13QWBHK5Bd0QNmn2gHsjt6wewbHWhsZDXIepHNRLYL2Q3IbkNxM5JfsPsXACkGXYWXfeLsAAAAAElFTkSuQmCC'; +var MIDDLE_POINT_ICON_URL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAMAAADzapwJAAAAP1BMVEUAAAArgNVVqv9OleNMkd1KjuJMj+BMlOBZmudYmORTluJTluZ0qupPlOFtp+hdm+aZwu9HjuDp8vv7/f39//2ENvQ5AAAAFXRSTlMABgYkJTQ5OT9DUFBsbm5wcHNzc3NuwP+LAAAAZ0lEQVR42r2PRw7AIAwETcCQYkL9/1sTgUKLxJE5jlZrL6xgE4hiG63UZC1p2dvD+fjizdlaZUJMBNfkmU7ZlNe1n1MskCgabdUW5/pfMj8Jyn0Pmm7Qdec5bocOlccrGGAckTNYwAMlgQk+DTSqnAAAAABJRU5ErkJggg=='; + +var Point = GeometryBase.extend({ + defaults: { + editable: false, + expandable: false, + iconUrl: DEFAULT_ICON_URL, + iconAnchor: [ 11, 11 ] + }, + + getCoordinates: function () { + return this.get('latlng'); + }, + + setCoordinates: function (latlng) { + this.set('latlng', latlng); + }, + + update: function (latlng) { + if (!this.get('latlng')) { + this.setCoordinates(latlng); + } + }, + + isComplete: function () { + return !!this.get('latlng'); + }, + + toGeoJSON: function () { + var coords = GeoJSONHelper.convertLatLngsToGeoJSONPointCoords(this.getCoordinates()); + return { + 'type': 'Point', + 'coordinates': coords + }; + }, + + getCoordinatesFromGeoJSONCoords: function (geoJSON) { + return GeoJSONHelper.getPointLatLngFromGeoJSONCoords(geoJSON); + } +}, { + DEFAULT_ICON_URL: DEFAULT_ICON_URL, + MIDDLE_POINT_ICON_URL: MIDDLE_POINT_ICON_URL +}); + +module.exports = Point; diff --git a/src/geo/geometry-models/polygon.js b/src/geo/geometry-models/polygon.js new file mode 100644 index 0000000..4ed639f --- /dev/null +++ b/src/geo/geometry-models/polygon.js @@ -0,0 +1,38 @@ +var GeoJSONHelper = require('./geojson-helper'); +var PathBase = require('./path-base'); + +var Polygon = PathBase.extend({ + defaults: { + editable: false, + expandable: false, + lineColor: '#397dba', + lineWeight: '4', + lineOpacity: '0.5' + }, + + MIN_NUMBER_OF_VERTICES: 3, + + isComplete: function () { + return this.points.length >= this.MIN_NUMBER_OF_VERTICES; + }, + + getCoordinatesForMiddlePoints: function () { + var coordinates = this.getCoordinates(); + coordinates.push(coordinates[0]); + return coordinates; + }, + + toGeoJSON: function () { + var coords = GeoJSONHelper.convertLatLngsToGeoJSONPolygonCoords(this.getCoordinates()); + return { + 'type': 'Polygon', + 'coordinates': [ coords ] + }; + }, + + getCoordinatesFromGeoJSONCoords: function (geoJSON) { + return GeoJSONHelper.getPolygonLatLngsFromGeoJSONCoords(geoJSON); + } +}); + +module.exports = Polygon; diff --git a/src/geo/geometry-models/polyline.js b/src/geo/geometry-models/polyline.js new file mode 100644 index 0000000..5200346 --- /dev/null +++ b/src/geo/geometry-models/polyline.js @@ -0,0 +1,32 @@ +var GeoJSONHelper = require('./geojson-helper'); +var PathBase = require('./path-base'); + +var Polyline = PathBase.extend({ + defaults: { + editable: false, + expandable: false, + lineColor: '#397dba', + lineWeight: '4', + lineOpacity: '0.5' + }, + + MIN_NUMBER_OF_VERTICES: 2, + + isComplete: function () { + return this.points.length >= this.MIN_NUMBER_OF_VERTICES; + }, + + toGeoJSON: function () { + var coords = GeoJSONHelper.convertLatLngsToGeoJSONPolylineCoords(this.getCoordinates()); + return { + 'type': 'LineString', + 'coordinates': coords + }; + }, + + getCoordinatesFromGeoJSONCoords: function (geoJSON) { + return GeoJSONHelper.getPolylineLatLngsFromGeoJSONCoords(geoJSON); + } +}); + +module.exports = Polyline; diff --git a/src/geo/geometry-views/base/geometry-view-base.js b/src/geo/geometry-views/base/geometry-view-base.js new file mode 100644 index 0000000..352b969 --- /dev/null +++ b/src/geo/geometry-views/base/geometry-view-base.js @@ -0,0 +1,18 @@ +var View = require('../../../core/view'); + +var GeometryViewBase = View.extend({ + initialize: function (options) { + if (!options.model) throw new Error('model is required'); + if (!options.mapView) throw new Error('mapView is required'); + + this.mapView = options.mapView; + + this.model.on('remove', this._onGeometryRemoved, this); + }, + + _onGeometryRemoved: function () { + this.clean(); + } +}); + +module.exports = GeometryViewBase; diff --git a/src/geo/geometry-views/base/marker-adapter-base.js b/src/geo/geometry-views/base/marker-adapter-base.js new file mode 100644 index 0000000..96bb8a2 --- /dev/null +++ b/src/geo/geometry-views/base/marker-adapter-base.js @@ -0,0 +1,47 @@ +var MarkerAdapterBase = function (nativeMarker) {}; + +MarkerAdapterBase.prototype.addToMap = function (nativeMap) { + throw new Error('subclasses of MarkerAdapterBase must implement addToMap'); +}; + +MarkerAdapterBase.prototype.removeFromMap = function (nativeMap) { + throw new Error('subclasses of MarkerAdapterBase must implement removeFromMap'); +}; + +MarkerAdapterBase.prototype.isAddedToMap = function (leafletMap) { + throw new Error('subclasses of MarkerAdapterBase must implement isAddedToMap'); +}; + +MarkerAdapterBase.prototype.getCoordinates = function () { + throw new Error('subclasses of MarkerAdapterBase must implement getCoordinates'); +}; + +MarkerAdapterBase.prototype.setCoordinates = function (coordinates) { + throw new Error('subclasses of MarkerAdapterBase must implement setCoordinates'); +}; + +MarkerAdapterBase.prototype.isDraggable = function () { + throw new Error('subclasses of MarkerAdapterBase must implement isDraggable'); +}; + +MarkerAdapterBase.prototype.getIconURL = function () { + throw new Error('subclasses of MarkerAdapterBase must implement getIconURL'); +}; + +MarkerAdapterBase.prototype.setIcon = function (iconURL, iconAnchor) { + throw new Error('subclasses of MarkerAdapterBase must implement setIcon'); +}; + +MarkerAdapterBase.prototype.on = function () { + throw new Error('subclasses of MarkerAdapterBase must implement on'); +}; + +MarkerAdapterBase.prototype.off = function () { + throw new Error('subclasses of MarkerAdapterBase must implement off'); +}; + +MarkerAdapterBase.prototype.trigger = function () { + throw new Error('subclasses of MarkerAdapterBase must implement trigger'); +}; + +module.exports = MarkerAdapterBase; diff --git a/src/geo/geometry-views/base/multi-geometry-view-base.js b/src/geo/geometry-views/base/multi-geometry-view-base.js new file mode 100644 index 0000000..a5e6207 --- /dev/null +++ b/src/geo/geometry-views/base/multi-geometry-view-base.js @@ -0,0 +1,27 @@ +var GeometryViewBase = require('./geometry-view-base'); + +var MultiGeometryViewBase = GeometryViewBase.extend({ + initialize: function (options) { + GeometryViewBase.prototype.initialize.apply(this, arguments); + if (!this.GeometryViewClass) throw new Error('subclasses of MultiGeometryViewBase must declare the GeometryViewClass instance variable'); + this.model.geometries.on('reset', this._renderGeometries, this); + }, + + render: function () { + this._renderGeometries(); + }, + + _renderGeometries: function () { + this.model.geometries.each(this._renderGeometry, this); + }, + + _renderGeometry: function (geometry) { + var polygonView = new this.GeometryViewClass({ + model: geometry, + mapView: this.mapView + }); + polygonView.render(); + } +}); + +module.exports = MultiGeometryViewBase; diff --git a/src/geo/geometry-views/base/path-adapter-base.js b/src/geo/geometry-views/base/path-adapter-base.js new file mode 100644 index 0000000..041def4 --- /dev/null +++ b/src/geo/geometry-views/base/path-adapter-base.js @@ -0,0 +1,23 @@ +var PathAdapterBase = function (nativePath) {}; + +PathAdapterBase.prototype.addToMap = function (nativeMap) { + throw new Error('subclasses of PathAdapterBase must implement addToMap'); +}; + +PathAdapterBase.prototype.removeFromMap = function (nativeMap) { + throw new Error('subclasses of PathAdapterBase must implement removeFromMap'); +}; + +PathAdapterBase.prototype.isAddedToMap = function (nativeMap) { + throw new Error('subclasses of PathAdapterBase must implement isAddedToMap'); +}; + +PathAdapterBase.prototype.getCoordinates = function () { + throw new Error('subclasses of PathAdapterBase must implement getCoordinates'); +}; + +PathAdapterBase.prototype.setCoordinates = function (coordinates) { + throw new Error('subclasses of PathAdapterBase must implement setCoordinates'); +}; + +module.exports = PathAdapterBase; diff --git a/src/geo/geometry-views/base/path-view-base.js b/src/geo/geometry-views/base/path-view-base.js new file mode 100644 index 0000000..32cd0cf --- /dev/null +++ b/src/geo/geometry-views/base/path-view-base.js @@ -0,0 +1,207 @@ +var _ = require('underscore'); +var GeometryViewBase = require('./geometry-view-base'); +var Point = require('../../geometry-models/point.js'); + +var computeMidLatLng = function (mapView, latLngA, latLngB) { + var leftPoint = mapView.latLngToContainerPoint(latLngA); + var rightPoint = mapView.latLngToContainerPoint(latLngB); + var y = (leftPoint.y + rightPoint.y) / 2; + var x = (leftPoint.x + rightPoint.x) / 2; + var latlng = mapView.containerPointToLatLng({ x: x, y: y }); + return [ latlng.lat, latlng.lng ]; +}; + +var PathViewBase = GeometryViewBase.extend({ + initialize: function (options) { + GeometryViewBase.prototype.initialize.apply(this, arguments); + + if (!this.PointViewClass) throw new Error('subclasses of PathViewBase must declare the PointViewClass instance variable'); + + this.model.points.on('change:latlng', this._onPointsChanged, this); + this.model.points.on('add', this._onPointAdded, this); + this.model.points.on('remove', this._onPointRemoved, this); + this.model.points.on('reset', this._onPointsReset, this); + this.add_related_model(this.model.points); + + this._geometry = this._createGeometry(); + + this._pointViews = {}; + this._middlePointViews = {}; + + _.bindAll(this, '_renderMiddlePoints'); + + this.mapView.on('zoomend', this._renderMiddlePoints); + }, + + _createGeometry: function () { + throw new Error('Subclasses of MyLeafletPathViewBase must implement _createGeometry'); + }, + + render: function () { + this._renderPoints(); + this._renderMiddlePoints(); + + this.mapView.addPath(this._geometry); + }, + + _renderPoints: function (points) { + points = points || this.model.points.models; + _.each(points, this._renderPoint, this); + }, + + _renderPoint: function (point) { + var pointView = this._tryToReuseMiddlePointView(point); + if (pointView) { + delete this._middlePointViews[point.cid]; + } else { + pointView = this._createPointView(point); + } + this.addView(pointView); + this._pointViews[point.cid] = pointView; + pointView.render(); + }, + + _tryToReuseMiddlePointView: function (point) { + var pointView; + + // If there's a view for middle point -> resuse the native geometry + if (this._middlePointViewForNewPoint) { + // "Transform" the middle point marker to a regular point marker + point.set({ + iconUrl: Point.prototype.defaults.iconUrl + }); + + pointView = new this.PointViewClass({ + model: point, + mapView: this.mapView, + marker: this._middlePointViewForNewPoint.getMarker() + }); + + // we're reusing the marker and we don't want this view + // to remove it from the map, so we unset it before cleaning + // the view + this._middlePointViewForNewPoint.unsetMarker(); + this._middlePointViewForNewPoint.clean(); + delete this._middlePointViewForNewPoint; + } + + return pointView; + }, + + _renderMiddlePoints: function () { + if (this.model.isExpandable()) { + this._clearMiddlePoints(); + var middlePoints = this._calculateMiddlePoints(); + _.each(middlePoints, this._renderMiddlePoint, this); + } + }, + + _calculateMiddlePoints: function () { + var coordinates = this.model.getCoordinatesForMiddlePoints(); + var mapView = this.mapView; + return _.map(coordinates.slice(0, -1), function (latLngA, index) { + var latLngB = coordinates[index + 1]; + + return new Point({ + latlng: computeMidLatLng(mapView, latLngA, latLngB), + editable: true, + iconUrl: Point.MIDDLE_POINT_ICON_URL + }); + }); + }, + + _renderMiddlePoint: function (point, index) { + var pointView = this._createPointView(point); + + point.once('dblclick', function (point) { + this.model.removePoint(point); + }.bind(this)); + + this._middlePointViews[point.cid] = pointView; + pointView.render(); + + pointView.once('mousedown', function (point) { + this._middlePointViewForNewPoint = pointView; + + // Add the coordinates of the new point to the path + this.model.addPoint(point, { at: index + 1 }); + }.bind(this)); + + this.addView(pointView); + }, + + _clearMiddlePoints: function () { + _.each(this._middlePointViews, function (view, point) { + view.clean(); + }); + }, + + _createPointView: function (point) { + var pointView = new this.PointViewClass({ + model: point, + mapView: this.mapView + }); + + return pointView; + }, + + _onPointsChanged: function () { + this._renderMiddlePoints(); + + this._updatePathFromModel(); + }, + + _onPointAdded: function (point) { + this._renderPoints([ point ]); + this._renderMiddlePoints(); + + this._updatePathFromModel(); + }, + + _onPointRemoved: function (point) { + this._removePoints([ point ]); + this._renderMiddlePoints(); + + this._updatePathFromModel(); + }, + + _onPointsReset: function (collection, options) { + var previousPoints = options.previousModels; + var newPoints = this.model.points.models; + var pointsToRemove = _.difference(previousPoints, newPoints); + var pointsToAdd = _.difference(newPoints, previousPoints); + + this._removePoints(pointsToRemove); + this._renderPoints(pointsToAdd); + this._renderMiddlePoints(); + + this._updatePathFromModel(); + }, + + _removePoints: function (points) { + points = points || this.model.points.models; + _.each(points, function (point) { + var pointView = this._pointViews[point.cid]; + if (pointView) { + pointView.clean(); + delete this._pointViews[point.cid]; + } + }, this); + }, + + _updatePathFromModel: function () { + this._geometry.setCoordinates(this.model.getCoordinates()); + }, + + clean: function () { + GeometryViewBase.prototype.clean.call(this); + this.mapView.removePath(this._geometry); + this.mapView.off('zoomend', this._renderMiddlePoints); + }, + + _getPointKey: function (point) { + return point.cid; + } +}); + +module.exports = PathViewBase; diff --git a/src/geo/geometry-views/base/point-view-base.js b/src/geo/geometry-views/base/point-view-base.js new file mode 100644 index 0000000..764d93b --- /dev/null +++ b/src/geo/geometry-views/base/point-view-base.js @@ -0,0 +1,151 @@ +var _ = require('underscore'); +var GeometryViewBase = require('./geometry-view-base'); + +var DRAG_DEBOUNCE_TIME_IN_MILIS = 10; + +var PointViewBase = GeometryViewBase.extend({ + initialize: function (options) { + GeometryViewBase.prototype.initialize.apply(this, arguments); + this.model.on('change:latlng', this._onLatlngChanged, this); + this.model.on('change:iconUrl change:iconAnchor', this._updateMarkersIcon, this); + + this._marker = null; + if (options.marker) { + this._marker = options.marker; + this._updateMarkersIcon(); + } + + // This method is debounced and we need to initialize it here so that: + // 1. Binding/unbinding can use the debounced function as the callback. + // 2. Debouncing can be easily disabled in the tests + this._onDrag = _.debounce(this._updateModelFromMarker.bind(this), DRAG_DEBOUNCE_TIME_IN_MILIS); + + _.bindAll(this, '_onDragStart', '_onDrag', '_onDragEnd', '_onMouseDown', '_onMouseClick', '_onMouseDblclick'); + }, + + getMarker: function () { + return this._marker; + }, + + unsetMarker: function () { + this._unbindMarkerEvents(); + delete this._marker; + }, + + _onLatlngChanged: function () { + this._renderMarkerIfNotRendered(); + + if (!this.isDragging()) { + this._updateMarkerFromModel(); + } + }, + + render: function () { + if (this.model.get('latlng')) { + this._renderMarkerIfNotRendered(); + } + }, + + _renderMarkerIfNotRendered: function () { + var isEditable = this.model.isEditable(); + if (this._isMarkerRendered()) { + this._unbindMarkerEvents(); + } else { + this._marker = this._createMarker(); + this.mapView.addMarker(this._marker); + } + + if (isEditable) { + this._bindMarkerEvents(); + } + }, + + _isMarkerRendered: function () { + return this._marker && this.mapView.hasMarker(this._marker); + }, + + _removeMarker: function () { + if (this._marker) { + // this._unbindMarkerEvents(); + this.mapView.removeMarker(this._marker); + } + }, + + _onDragStart: function () { + this._isDragging = true; + }, + + _onDragEnd: function () { + this._isDragging = false; + }, + + _onMouseDown: function () { + this._mouseDownClicked = true; + this.trigger('mousedown', this.model); + }, + + _onMouseClick: function () { + // Some point views reuse existing markers. + // We want to only trigger the 'click' event if marker + // was clicked while being associated to this view. + if (this._mouseDownClicked) { + this.trigger('click', this.model); + this._mouseDownClicked = false; + } + }, + + _onMouseDblclick: function () { + if (this._mouseDownClicked) { + this.model.trigger('dblclick', this.model); + this._mouseDownClicked = false; + } + }, + + isDragging: function () { + return !!this._isDragging; + }, + + clean: function () { + GeometryViewBase.prototype.clean.apply(this); + this._removeMarker(); + }, + + _updateModelFromMarker: function () { + if (this._marker) { + var latLng = this._marker.getCoordinates(); + this.model.setCoordinates([ latLng.lat, latLng.lng ]); + } + }, + + _updateMarkerFromModel: function () { + this._marker.setCoordinates(this.model.getCoordinates()); + }, + + _updateMarkersIcon: function () { + if (this._marker) { + this._marker.setIconURL(this.model.get('iconUrl'), this.model.get('iconAnchor')); + } + }, + + _createMarker: function () { + throw new Error('subclasses of PointViewBase must implement _createMarker'); + }, + + _unbindMarkerEvents: function () { + this._marker.off('mousedown', this._onMouseDown); + this._marker.off('dblclick', this._onMouseDblclick); + this._marker.off('dragstart', this._onDragStart); + this._marker.off('drag', this._onDrag); + this._marker.off('dragend', this._onDragEnd); + }, + + _bindMarkerEvents: function () { + this._marker.on('mousedown', this._onMouseDown); + this._marker.on('dblclick', this._onMouseDblclick); + this._marker.on('dragstart', this._onDragStart); + this._marker.on('drag', this._onDrag); + this._marker.on('dragend', this._onDragEnd); + } +}); + +module.exports = PointViewBase; diff --git a/src/geo/geometry-views/geometry-view-factory.js b/src/geo/geometry-views/geometry-view-factory.js new file mode 100644 index 0000000..9a75405 --- /dev/null +++ b/src/geo/geometry-views/geometry-view-factory.js @@ -0,0 +1,90 @@ +var Map = require('../map.js'); + +var Point = require('../geometry-models/point'); +var Polyline = require('../geometry-models/polyline'); +var Polygon = require('../geometry-models/polygon'); +var MultiPoint = require('../geometry-models/multi-point'); +var MultiPolygon = require('../geometry-models/multi-polygon'); +var MultiPolyline = require('../geometry-models/multi-polyline'); + +var getGeometryViewForGeometry = function (provider, geometry) { + var GeometryView; + if (provider === Map.PROVIDERS.GMAPS) { + GeometryView = getGoogleMapsGeometryViewForGeometry(geometry); + } else if (provider === Map.PROVIDERS.LEAFLET) { + GeometryView = getLeafletGeometryViewForGeometry(geometry); + } else { + throw new Error("unknown provider: '" + provider); + } + return GeometryView; +}; + +/* Leaflet */ + +var LeafletPointView = require('./leaflet/point-view'); +var LeafletPolygonView = require('./leaflet/polygon-view'); +var LeafletPolylineView = require('./leaflet/polyline-view'); +var LeafletMultiPointView = require('./leaflet/multi-point-view'); +var LeafletMultiPolygonView = require('./leaflet/multi-polygon-view'); +var LeafletMultiPolylineView = require('./leaflet/multi-polyline-view'); + +var getLeafletGeometryViewForGeometry = function (geometry) { + var GeometryView; + if (geometry instanceof Point) { + GeometryView = LeafletPointView; + } else if (geometry instanceof Polyline) { + GeometryView = LeafletPolylineView; + } else if (geometry instanceof Polygon) { + GeometryView = LeafletPolygonView; + } else if (geometry instanceof MultiPoint) { + GeometryView = LeafletMultiPointView; + } else if (geometry instanceof MultiPolyline) { + GeometryView = LeafletMultiPolylineView; + } else if (geometry instanceof MultiPolygon) { + GeometryView = LeafletMultiPolygonView; + } + return GeometryView; +}; + +/* Google Maps */ + +var GoogleMapsPointView = require('./gmaps/point-view'); +var GoogleMapsPolygonView = require('./gmaps/polygon-view'); +var GoogleMapsPolylineView = require('./gmaps/polyline-view'); +var GoogleMapsMultiPointView = require('./gmaps/multi-point-view'); +var GoogleMapsMultiPolygonView = require('./gmaps/multi-polygon-view'); +var GoogleMapsMultiPolylineView = require('./gmaps/multi-polyline-view'); + +var getGoogleMapsGeometryViewForGeometry = function (geometry) { + var GeometryView; + if (geometry instanceof Point) { + GeometryView = GoogleMapsPointView; + } else if (geometry instanceof Polyline) { + GeometryView = GoogleMapsPolylineView; + } else if (geometry instanceof Polygon) { + GeometryView = GoogleMapsPolygonView; + } else if (geometry instanceof MultiPoint) { + GeometryView = GoogleMapsMultiPointView; + } else if (geometry instanceof MultiPolyline) { + GeometryView = GoogleMapsMultiPolylineView; + } else if (geometry instanceof MultiPolygon) { + GeometryView = GoogleMapsMultiPolygonView; + } + return GeometryView; +}; + +var GeometryViewFactory = { + createGeometryView: function (provider, geometry, mapView) { + var GeometryView = getGeometryViewForGeometry(provider, geometry); + if (GeometryView) { + return new GeometryView({ + model: geometry, + mapView: mapView + }); + } + + throw new Error(geometry.get('type') + ' is not supported yet'); + } +}; + +module.exports = GeometryViewFactory; diff --git a/src/geo/geometry-views/gmaps/gmaps-coordinates.js b/src/geo/geometry-views/gmaps/gmaps-coordinates.js new file mode 100644 index 0000000..a2f2e31 --- /dev/null +++ b/src/geo/geometry-views/gmaps/gmaps-coordinates.js @@ -0,0 +1,12 @@ +var _ = require('underscore'); + +module.exports = { + convertToGMapsCoordinates: function (coordinates) { + return _.map(coordinates, function (coordinate) { + return { + lat: coordinate[0], + lng: coordinate[1] + }; + }); + } +}; diff --git a/src/geo/geometry-views/gmaps/gmaps-marker-adapter.js b/src/geo/geometry-views/gmaps/gmaps-marker-adapter.js new file mode 100644 index 0000000..c5fd63a --- /dev/null +++ b/src/geo/geometry-views/gmaps/gmaps-marker-adapter.js @@ -0,0 +1,70 @@ +/* global google */ +var MarkerAdapterBase = require('../base/marker-adapter-base'); + +var GMapsMarkerAdapter = function (nativeMarker) { + this._listeners = {}; + this._nativeMarker = nativeMarker; +}; + +GMapsMarkerAdapter.prototype = new MarkerAdapterBase(); +GMapsMarkerAdapter.prototype.constructor = GMapsMarkerAdapter; + +GMapsMarkerAdapter.prototype.addToMap = function (gMapsMap) { + this._nativeMarker.setMap(gMapsMap); +}; + +GMapsMarkerAdapter.prototype.removeFromMap = function (gMapsMap) { + this._nativeMarker.setMap(null); +}; + +GMapsMarkerAdapter.prototype.isAddedToMap = function (gMapsMap) { + return !!this._nativeMarker.getMap(); +}; + +GMapsMarkerAdapter.prototype.getCoordinates = function () { + var position = this._nativeMarker.getPosition(); + return { + lat: position.lat(), + lng: position.lng() + }; +}; + +GMapsMarkerAdapter.prototype.setCoordinates = function (coordinates) { + var position = new google.maps.LatLng(coordinates[0], coordinates[1]); + this._nativeMarker.setPosition(position); +}; + +GMapsMarkerAdapter.prototype.isDraggable = function () { + return this._nativeMarker.getDraggable(); +}; + +GMapsMarkerAdapter.prototype.getIconURL = function () { + return (this._nativeMarker.getIcon() && this._nativeMarker.getIcon().url) || + this._nativeMarker.getIcon(); +}; + +GMapsMarkerAdapter.prototype.setIconURL = function (iconURL, iconAnchor) { + var icon = { + url: iconURL, + origin: new google.maps.Point(0, 0), + anchor: new google.maps.Point(iconAnchor[0], iconAnchor[1]) + }; + this._nativeMarker.setIcon(icon); +}; + +GMapsMarkerAdapter.prototype.on = function (eventName, callback) { + var listener = this._nativeMarker.addListener(eventName, callback); + this._listeners[eventName] = listener; +}; + +GMapsMarkerAdapter.prototype.off = function (eventName) { + var listener = this._listeners[eventName]; + google.maps.event.removeListener(listener); +}; + +GMapsMarkerAdapter.prototype.trigger = function (eventName) { + var eventArgs = Array.prototype.slice.call(arguments, 1); + google.maps.event.trigger(this._nativeMarker, eventName, eventArgs); +}; + +module.exports = GMapsMarkerAdapter; diff --git a/src/geo/geometry-views/gmaps/gmaps-path-adapter.js b/src/geo/geometry-views/gmaps/gmaps-path-adapter.js new file mode 100644 index 0000000..9b796e3 --- /dev/null +++ b/src/geo/geometry-views/gmaps/gmaps-path-adapter.js @@ -0,0 +1,39 @@ +var _ = require('underscore'); +var PathAdapterBase = require('../base/path-adapter-base'); +var GMapsCoordinates = require('./gmaps-coordinates'); + +var GMapsPathAdapter = function (nativePath) { + this._nativePath = nativePath; +}; + +GMapsPathAdapter.prototype = new PathAdapterBase(); +GMapsPathAdapter.prototype.constructor = GMapsPathAdapter; + +GMapsPathAdapter.prototype.addToMap = function (gMapsMap) { + this._nativePath.setMap(gMapsMap); +}; + +GMapsPathAdapter.prototype.removeFromMap = function (gMapsMap) { + this._nativePath.setMap(null); +}; + +GMapsPathAdapter.prototype.isAddedToMap = function (gMapsMap) { + return !!this._nativePath.getMap(); +}; + +GMapsPathAdapter.prototype.getCoordinates = function () { + var path = this._nativePath.getPath(); + return _.map(path.getArray(), function (coordinates) { + return { + lat: coordinates.lat(), + lng: coordinates.lng() + }; + }); +}; + +GMapsPathAdapter.prototype.setCoordinates = function (coordinates) { + var path = GMapsCoordinates.convertToGMapsCoordinates(coordinates); + this._nativePath.setPath(path); +}; + +module.exports = GMapsPathAdapter; diff --git a/src/geo/geometry-views/gmaps/multi-point-view.js b/src/geo/geometry-views/gmaps/multi-point-view.js new file mode 100644 index 0000000..8793d73 --- /dev/null +++ b/src/geo/geometry-views/gmaps/multi-point-view.js @@ -0,0 +1,8 @@ +var MultiGeometryViewBase = require('../base/multi-geometry-view-base'); +var Pointview = require('./point-view'); + +var MultiPolygonView = MultiGeometryViewBase.extend({ + GeometryViewClass: Pointview +}); + +module.exports = MultiPolygonView; diff --git a/src/geo/geometry-views/gmaps/multi-polygon-view.js b/src/geo/geometry-views/gmaps/multi-polygon-view.js new file mode 100644 index 0000000..cdad0ad --- /dev/null +++ b/src/geo/geometry-views/gmaps/multi-polygon-view.js @@ -0,0 +1,8 @@ +var MultiGeometryViewBase = require('../base/multi-geometry-view-base'); +var PolygonView = require('./polygon-view'); + +var MultiPolygonView = MultiGeometryViewBase.extend({ + GeometryViewClass: PolygonView +}); + +module.exports = MultiPolygonView; diff --git a/src/geo/geometry-views/gmaps/multi-polyline-view.js b/src/geo/geometry-views/gmaps/multi-polyline-view.js new file mode 100644 index 0000000..f20e6bc --- /dev/null +++ b/src/geo/geometry-views/gmaps/multi-polyline-view.js @@ -0,0 +1,8 @@ +var MultiGeometryViewBase = require('../base/multi-geometry-view-base'); +var PolylineView = require('./polyline-view'); + +var MultiPolygonView = MultiGeometryViewBase.extend({ + GeometryViewClass: PolylineView +}); + +module.exports = MultiPolygonView; diff --git a/src/geo/geometry-views/gmaps/point-view.js b/src/geo/geometry-views/gmaps/point-view.js new file mode 100644 index 0000000..b3a07d8 --- /dev/null +++ b/src/geo/geometry-views/gmaps/point-view.js @@ -0,0 +1,27 @@ +/* global google */ +var PointViewBase = require('../base/point-view-base.js'); +var GMapsMarkerAdapter = require('./gmaps-marker-adapter'); + +var PointView = PointViewBase.extend({ + _createMarker: function () { + var position = new google.maps.LatLng(this.model.getCoordinates()[0], this.model.getCoordinates()[1]); + var icon = { + url: this.model.get('iconUrl'), + origin: new google.maps.Point(0, 0), + anchor: new google.maps.Point(this.model.get('iconAnchor')[0], this.model.get('iconAnchor')[1]) + }; + + var marker = new google.maps.Marker({ + position: position, + icon: icon, + draggable: this.model.isEditable(), + crossOnDrag: false + }); + + // iconAnchor: this.model.get('iconAnchor') + + return new GMapsMarkerAdapter(marker); + } +}); + +module.exports = PointView; diff --git a/src/geo/geometry-views/gmaps/polygon-view.js b/src/geo/geometry-views/gmaps/polygon-view.js new file mode 100644 index 0000000..555e3e1 --- /dev/null +++ b/src/geo/geometry-views/gmaps/polygon-view.js @@ -0,0 +1,23 @@ +/* global google */ +var PathViewBase = require('../base/path-view-base'); +var PointView = require('./point-view'); +var GMapsPathAdapter = require('./gmaps-path-adapter'); +var GMapsCoordinates = require('./gmaps-coordinates'); + +var PolygonView = PathViewBase.extend({ + PointViewClass: PointView, + + _createGeometry: function () { + var paths = GMapsCoordinates.convertToGMapsCoordinates(this.model.getCoordinates()); + var polygon = new google.maps.Polygon({ + paths: paths, + strokeColor: this.model.get('lineColor'), + strokeWeight: this.model.get('lineWeight'), + strokeOpacity: this.model.get('lineOpacity') + }); + + return new GMapsPathAdapter(polygon); + } +}); + +module.exports = PolygonView; diff --git a/src/geo/geometry-views/gmaps/polyline-view.js b/src/geo/geometry-views/gmaps/polyline-view.js new file mode 100644 index 0000000..5ef4c08 --- /dev/null +++ b/src/geo/geometry-views/gmaps/polyline-view.js @@ -0,0 +1,23 @@ +/* global google */ +var PathViewBase = require('../base/path-view-base'); +var PointView = require('./point-view'); +var GMapsPathAdapter = require('./gmaps-path-adapter'); +var GMapsCoordinates = require('./gmaps-coordinates'); + +var PolylineView = PathViewBase.extend({ + PointViewClass: PointView, + + _createGeometry: function () { + var path = GMapsCoordinates.convertToGMapsCoordinates(this.model.getCoordinates()); + var polyline = new google.maps.Polyline({ + path: path, + strokeColor: this.model.get('lineColor'), + strokeWeight: this.model.get('lineWeight'), + strokeOpacity: this.model.get('lineOpacity') + }); + + return new GMapsPathAdapter(polyline); + } +}); + +module.exports = PolylineView; diff --git a/src/geo/geometry-views/leaflet/leaflet-marker-adapter.js b/src/geo/geometry-views/leaflet/leaflet-marker-adapter.js new file mode 100644 index 0000000..316e2ee --- /dev/null +++ b/src/geo/geometry-views/leaflet/leaflet-marker-adapter.js @@ -0,0 +1,65 @@ +var MarkerAdapterBase = require('../base/marker-adapter-base'); + +var LeafletMarkerAdapter = function (nativeMarker) { + this._nativeMarker = nativeMarker; +}; + +LeafletMarkerAdapter.prototype = new MarkerAdapterBase(); +LeafletMarkerAdapter.prototype.constructor = LeafletMarkerAdapter; + +LeafletMarkerAdapter.prototype.addToMap = function (leafletMap) { + leafletMap.addLayer(this._nativeMarker); +}; + +LeafletMarkerAdapter.prototype.removeFromMap = function (leafletMap) { + leafletMap.removeLayer(this._nativeMarker); +}; + +LeafletMarkerAdapter.prototype.isAddedToMap = function (leafletMap) { + return leafletMap.hasLayer(this._nativeMarker); +}; + +LeafletMarkerAdapter.prototype.getCoordinates = function () { + var latLng = this._nativeMarker.getLatLng(); + return { + lat: latLng.lat, + lng: latLng.lng + }; +}; + +LeafletMarkerAdapter.prototype.setCoordinates = function (coordinates) { + this._nativeMarker.setLatLng(coordinates); +}; + +LeafletMarkerAdapter.prototype.isDraggable = function () { + return this._nativeMarker.options.draggable; +}; + +LeafletMarkerAdapter.prototype.getIconURL = function () { + return this._nativeMarker.options.icon.options.iconUrl; +}; + +LeafletMarkerAdapter.prototype.setIconURL = function (iconURL) { + // Leaflet provides the `setIcon` method which generates a new img for the icon. + // We want to be able to update the markers icon while it's being dragged. That's + // why we're doing this little hack that changes the src of the existing marker img. + // Removing the image and adding a new one we lost the mousedown event so drag is not + // working, reusing the node, we avoid some extra browser work and reuse the attached + // events, having the desired behaviour. + this._nativeMarker._icon.src = iconURL; + this._nativeMarker.options.icon.options.iconUrl = iconURL; +}; + +LeafletMarkerAdapter.prototype.on = function () { + this._nativeMarker.on.apply(this._nativeMarker, arguments); +}; + +LeafletMarkerAdapter.prototype.off = function () { + this._nativeMarker.off.apply(this._nativeMarker, arguments); +}; + +LeafletMarkerAdapter.prototype.trigger = function () { + this._nativeMarker.fire.apply(this._nativeMarker, arguments); +}; + +module.exports = LeafletMarkerAdapter; diff --git a/src/geo/geometry-views/leaflet/leaflet-path-adapter.js b/src/geo/geometry-views/leaflet/leaflet-path-adapter.js new file mode 100644 index 0000000..a2bbac7 --- /dev/null +++ b/src/geo/geometry-views/leaflet/leaflet-path-adapter.js @@ -0,0 +1,41 @@ +var _ = require('underscore'); +var PathAdapterBase = require('../base/path-adapter-base'); + +var LeafletPathAdapter = function (nativePath) { + this._nativePath = nativePath; +}; + +LeafletPathAdapter.prototype = new PathAdapterBase(); +LeafletPathAdapter.prototype.constructor = LeafletPathAdapter; + +LeafletPathAdapter.prototype.addToMap = function (leafletMap) { + leafletMap.addLayer(this._nativePath); +}; + +LeafletPathAdapter.prototype.removeFromMap = function (leafletMap) { + leafletMap.removeLayer(this._nativePath); +}; + +LeafletPathAdapter.prototype.isAddedToMap = function (leafletMap) { + return leafletMap.hasLayer(this._nativePath); +}; + +LeafletPathAdapter.prototype.getCoordinates = function () { + // L.Polygon#getLatLngs returns an array of arrays and + // L.Path#getLatLngs returns an array of coordinates but we're + // working with an array of coordinates internally and that's why + // we're flattening latlngs + var latlngs = _.flatten(this._nativePath.getLatLngs()); + return _.map(latlngs, function (latlng) { + return { + lat: latlng.lat, + lng: latlng.lng + }; + }); +}; + +LeafletPathAdapter.prototype.setCoordinates = function (coordinates) { + this._nativePath.setLatLngs(coordinates); +}; + +module.exports = LeafletPathAdapter; diff --git a/src/geo/geometry-views/leaflet/multi-point-view.js b/src/geo/geometry-views/leaflet/multi-point-view.js new file mode 100644 index 0000000..ed9d25c --- /dev/null +++ b/src/geo/geometry-views/leaflet/multi-point-view.js @@ -0,0 +1,8 @@ +var MultiGeometryViewBase = require('../base/multi-geometry-view-base'); +var PointView = require('./point-view'); + +var MultiPointView = MultiGeometryViewBase.extend({ + GeometryViewClass: PointView +}); + +module.exports = MultiPointView; diff --git a/src/geo/geometry-views/leaflet/multi-polygon-view.js b/src/geo/geometry-views/leaflet/multi-polygon-view.js new file mode 100644 index 0000000..cdad0ad --- /dev/null +++ b/src/geo/geometry-views/leaflet/multi-polygon-view.js @@ -0,0 +1,8 @@ +var MultiGeometryViewBase = require('../base/multi-geometry-view-base'); +var PolygonView = require('./polygon-view'); + +var MultiPolygonView = MultiGeometryViewBase.extend({ + GeometryViewClass: PolygonView +}); + +module.exports = MultiPolygonView; diff --git a/src/geo/geometry-views/leaflet/multi-polyline-view.js b/src/geo/geometry-views/leaflet/multi-polyline-view.js new file mode 100644 index 0000000..f20e6bc --- /dev/null +++ b/src/geo/geometry-views/leaflet/multi-polyline-view.js @@ -0,0 +1,8 @@ +var MultiGeometryViewBase = require('../base/multi-geometry-view-base'); +var PolylineView = require('./polyline-view'); + +var MultiPolygonView = MultiGeometryViewBase.extend({ + GeometryViewClass: PolylineView +}); + +module.exports = MultiPolygonView; diff --git a/src/geo/geometry-views/leaflet/point-view.js b/src/geo/geometry-views/leaflet/point-view.js new file mode 100644 index 0000000..756c09e --- /dev/null +++ b/src/geo/geometry-views/leaflet/point-view.js @@ -0,0 +1,21 @@ +/* global L */ +var PointViewBase = require('../base/point-view-base.js'); +var LeafletMarkerAdapter = require('./leaflet-marker-adapter'); + +var PointView = PointViewBase.extend({ + _createMarker: function () { + var isEditable = this.model.isEditable(); + var icon = L.icon({ + iconUrl: this.model.get('iconUrl'), + iconAnchor: this.model.get('iconAnchor') + }); + + var marker = L.marker(this.model.get('latlng'), { + icon: icon, + draggable: !!isEditable + }); + return new LeafletMarkerAdapter(marker); + } +}); + +module.exports = PointView; diff --git a/src/geo/geometry-views/leaflet/polygon-view.js b/src/geo/geometry-views/leaflet/polygon-view.js new file mode 100644 index 0000000..6d3825b --- /dev/null +++ b/src/geo/geometry-views/leaflet/polygon-view.js @@ -0,0 +1,19 @@ +/* global L */ +var PathViewBase = require('../base/path-view-base'); +var PointView = require('./point-view'); +var LeafletPathAdapter = require('./leaflet-path-adapter'); + +var PolygonView = PathViewBase.extend({ + PointViewClass: PointView, + + _createGeometry: function () { + var polygon = L.polygon(this.model.getCoordinates(), { + color: this.model.get('lineColor'), + weight: this.model.get('lineWeight'), + opacity: this.model.get('lineOpacity') + }); + return new LeafletPathAdapter(polygon); + } +}); + +module.exports = PolygonView; diff --git a/src/geo/geometry-views/leaflet/polyline-view.js b/src/geo/geometry-views/leaflet/polyline-view.js new file mode 100644 index 0000000..7b5e70c --- /dev/null +++ b/src/geo/geometry-views/leaflet/polyline-view.js @@ -0,0 +1,19 @@ +/* global L */ +var PathViewBase = require('../base/path-view-base'); +var PointView = require('./point-view'); +var LeafletPathAdapter = require('./leaflet-path-adapter'); + +var PolylineView = PathViewBase.extend({ + PointViewClass: PointView, + + _createGeometry: function () { + var polyline = L.polyline(this.model.getCoordinates(), { + color: this.model.get('lineColor'), + weight: this.model.get('lineWeight'), + opacity: this.model.get('lineOpacity') + }); + return new LeafletPathAdapter(polyline); + } +}); + +module.exports = PolylineView; diff --git a/src/geo/gmaps/gmaps-base-layer-view.js b/src/geo/gmaps/gmaps-base-layer-view.js new file mode 100644 index 0000000..459d9f8 --- /dev/null +++ b/src/geo/gmaps/gmaps-base-layer-view.js @@ -0,0 +1,44 @@ +/* global google */ +var _ = require('underscore'); +var GMapsLayerView = require('./gmaps-layer-view'); + +var GOOGLE_MAP_TYPE_IDS = { + 'roadmap': google.maps.MapTypeId.ROADMAP, + 'gray_roadmap': google.maps.MapTypeId.ROADMAP, + 'dark_roadmap': google.maps.MapTypeId.ROADMAP, + 'hybrid': google.maps.MapTypeId.HYBRID, + 'satellite': google.maps.MapTypeId.SATELLITE, + 'terrain': google.maps.MapTypeId.TERRAIN +}; + +var GMapsBaseLayerView = function (layerModel, opts) { + GMapsLayerView.apply(this, arguments); +}; + +_.extend( + GMapsBaseLayerView.prototype, + GMapsLayerView.prototype, { + addToMap: function () { + this._updateNativeMapOptions(); + }, + + remove: function () { }, + + _onModelUpdated: function () { + this._updateNativeMapOptions(); + }, + + _updateNativeMapOptions: function () { + var styles = {}; + try { + styles = JSON.parse(this.model.get('style')); + } catch (e) {} + this.gmapsMap.setOptions({ + mapTypeId: GOOGLE_MAP_TYPE_IDS[this.model.get('baseType')], + styles: styles + }); + } + } +); + +module.exports = GMapsBaseLayerView; diff --git a/src/geo/gmaps/gmaps-cartodb-layer-group-view.js b/src/geo/gmaps/gmaps-cartodb-layer-group-view.js new file mode 100644 index 0000000..c1f70ad --- /dev/null +++ b/src/geo/gmaps/gmaps-cartodb-layer-group-view.js @@ -0,0 +1,360 @@ +/* global Image, google */ +var _ = require('underscore'); +var GMapsLayerView = require('./gmaps-layer-view'); +var zera = require('@carto/zera'); + +var C = require('../../constants'); +var Projector = require('./projector'); +var CartoDBLayerGroupViewBase = require('../cartodb-layer-group-view-base'); +var Profiler = require('cdb.core.Profiler'); + +var OPACITY_FILTER = 'progid:DXImageTransform.Microsoft.gradient(startColorstr=#00FFFFFF,endColorstr=#00FFFFFF)'; +var EMPTY_GIF = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; + +function setImageOpacityIE8 (img, opacity) { + var v = Math.round(opacity * 100); + if (v >= 99) { + img.style.filter = OPACITY_FILTER; + } else { + img.style.filter = 'alpha(opacity=' + (opacity) + ');'; + } +} + +function generateId () { + return (((1 + Math.random()) * 0x100000000) | 0).toString(16).substring(1); +} + +var GMapsCartoDBLayerGroupView = function (layerModel, options) { + var self = this; + var hovers = []; + var gmapsMap = options.nativeMap; + + _.bindAll(this, 'featureOut', 'featureOver', 'featureClick'); + + var opts = _.clone(layerModel.attributes); + + opts.map = gmapsMap; + + var _featureOver = opts.featureOver; + var _featureOut = opts.featureOut; + var _featureClick = opts.featureClick; + + var previousEvent; + var eventTimeout = -1; + + opts.featureOver = function (e, latlon, pxPos, data, layer) { + if (!hovers[layer]) { + self.trigger('layerenter', e, latlon, pxPos, data, layer); + } + hovers[layer] = 1; + _featureOver && _featureOver.apply(this, arguments); + self.featureOver && self.featureOver.apply(this, arguments); + + // if the event is the same than before just cancel the event + // firing because there is a layer on top of it + if (e.timeStamp === previousEvent) { + clearTimeout(eventTimeout); + } + eventTimeout = setTimeout(function () { + self.trigger('mouseover', e, latlon, pxPos, data, layer); + self.trigger('layermouseover', e, latlon, pxPos, data, layer); + }, 0); + previousEvent = e.timeStamp; + }; + + opts.featureOut = function (m, layer) { + if (hovers[layer]) { + self.trigger('layermouseout', layer); + } + hovers[layer] = 0; + if (!_.any(hovers)) { + self.trigger('mouseout'); + } + _featureOut && _featureOut.apply(this, arguments); + self.featureOut && self.featureOut.apply(this, arguments); + }; + + opts.featureClick = _.debounce(function () { + _featureClick && _featureClick.apply(this, arguments); + self.featureClick && self.featureClick.apply(opts, arguments); + }, 10); + + this.tiles = 0; + + this.options = { + tiles: options.tiles, + scheme: options.scheme || 'xyz', + blankImage: options.blankImage || 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=' + }; + + // internal id + this._id = generateId(); + + // non-configurable options + this.interactive = true; + this.tileSize = new google.maps.Size(256, 256); + + // DOM element cache + this.cache = {}; + + _.extend(this.options, opts); + GMapsLayerView.apply(this, arguments); + this.projector = new Projector(opts.map); + CartoDBLayerGroupViewBase.apply(this, arguments); +}; + +GMapsCartoDBLayerGroupView.prototype.interactionClass = zera.Interactive; +_.extend( + GMapsCartoDBLayerGroupView.prototype, + CartoDBLayerGroupViewBase.prototype, + GMapsLayerView.prototype, + { + addToMap: function () { + this.gmapsMap.overlayMapTypes.setAt(0, this); + }, + + remove: function () { + var overlayIndex = this._getOverlayIndex(); + + if (overlayIndex >= 0) { + this.gmapsMap.overlayMapTypes.removeAt(overlayIndex); + } + + this._clearInteraction(); + this.finishLoading && this.finishLoading(); + }, + + reload: function () { + this.model.invalidate(); + }, + + featureOver: function (e, latlon, pixelPos, data, layer) { + var layerModel = this.model.getLayerInLayerGroupAt(layer); + if (layerModel) { + this.trigger('featureOver', { + layer: layerModel, + layerIndex: layer, + latlng: [latlon.lat(), latlon.lng()], + position: { x: pixelPos.x, y: pixelPos.y }, + feature: data + }); + } + }, + + featureOut: function (e, layer) { + var layerModel = this.model.getLayerInLayerGroupAt(layer); + if (layerModel) { + this.trigger('featureOut', { + layer: layerModel, + layerIndex: layer + }); + } + }, + + featureClick: function (e, latlon, pixelPos, data, layer) { + var layerModel = this.model.getLayerInLayerGroupAt(layer); + if (layerModel) { + this.trigger('featureClick', { + layer: layerModel, + layerIndex: layer, + latlng: [latlon.lat(), latlon.lng()], + position: { x: pixelPos.x, y: pixelPos.y }, + feature: data + }); + } + }, + + error: function (e) { + if (this.model) { + this.model.trigger('error', e ? e.errors : 'unknown error'); + this.model.trigger('tileError', e ? e.errors : 'unknown error'); + } + }, + + ok: function (e) { + this.model.trigger('tileOk'); + }, + + tilesOk: function (e) { + this.model.trigger('tileOk'); + }, + + loading: function () { + this.trigger('loading'); + }, + + finishLoading: function () { + this.trigger('load'); + }, + + setOpacity: function (opacity) { + if (isNaN(opacity) || opacity > 1 || opacity < 0) { + throw new Error(opacity + ' is not a valid value, should be in [0, 1] range'); + } + this.opacity = this.options.opacity = opacity; + for (var key in this.cache) { + var img = this.cache[key]; + img.style.opacity = opacity; + setImageOpacityIE8(img, opacity); + } + }, + + getTile: function (coord, zoom, ownerDocument) { + var self = this; + var ie = 'ActiveXObject' in window; + var ielt9 = ie && !document.addEventListener; + + this.options.added = true; + if (!this.model.hasTileURLTemplates()) { + var key = zoom + '/' + coord.x + '/' + coord.y; + var image = this.cache[key] = new Image(256, 256); + image.src = EMPTY_GIF; + image.setAttribute('gTileKey', key); + image.style.opacity = this.options.opacity; + return image; + } + + var tile = this._getTile(coord, zoom, ownerDocument); + + // in IE8 semi transparency does not work and needs filter + if (ielt9) { + setImageOpacityIE8(tile, this.options.opacity); + } + tile.style.opacity = this.options.opacity; + if (this.tiles === 0) { + this.loading && this.loading(); + } + + this.tiles++; + + var loadTime = Profiler.metric('cartodb-js.tile.png.load.time').start(); + + var finished = function () { + loadTime.end(); + self.tiles--; + if (self.tiles === 0) { + self.finishLoading && self.finishLoading(); + } + }; + + tile.onload = finished; + + tile.onerror = function () { + Profiler.metric('cartodb-js.tile.png.error').inc(); + self.model.addError({ type: C.WINDSHAFT_ERRORS.TILE }); + finished(); + }; + + return tile; + }, + + // Get a tile element from a coordinate, zoom level, and an ownerDocument. + _getTile: function (coord, zoom, ownerDocument) { + var key = zoom + '/' + coord.x + '/' + coord.y; + if (!this.cache[key]) { + var img = this.cache[key] = new Image(256, 256); + this.cache[key].src = this._getTileUrl(coord, zoom); + this.cache[key].setAttribute('gTileKey', key); + this.cache[key].onerror = function () { img.style.display = 'none'; }; + } + return this.cache[key]; + }, + + // Get a tile url, based on x, y coordinates and a z value. + _getTileUrl: function (coord, z) { + // Y coordinate is flipped in Mapbox, compared to Google + var mod = Math.pow(2, z); + var y = (this.options.scheme === 'tms') ? (mod - 1) - coord.y : coord.y; + var x = (coord.x % mod); + + x = (x < 0) ? (coord.x % mod) + mod : x; + + if (y < 0) return this.options.blankImage; + + return this.options.tiles[parseInt(x + y, 10) % this.options.tiles.length] + .replace(/\{z\}/g, z) + .replace(/\{x\}/g, x) + .replace(/\{y\}/g, y); + }, + + _reload: function () { + var tileURLTemplates; + if (this.model.hasTileURLTemplates()) { + tileURLTemplates = [this.model.getTileURLTemplatesWithSubdomains()[0]]; + } else { + tileURLTemplates = [EMPTY_GIF]; + } + + this.options.tiles = tileURLTemplates; + this.tiles = 0; + this.cache = {}; + this._reloadInteraction(); + this._refreshView(); + }, + + _refreshView: function () { + var overlays = this.gmapsMap.overlayMapTypes; + var overlayIndex = this._getOverlayIndex(); + + if (overlayIndex >= 0) { + overlays.setAt(overlayIndex, overlays.getAt(overlayIndex)); + } + }, + + _checkLayer: function () { + if (!this.options.added) { + throw new Error('the layer is not still added to the map'); + } + }, + + _getOverlayIndex: function () { + var overlays = this.gmapsMap.overlayMapTypes.getArray(); + + return _.findIndex(overlays, function (overlay) { + return overlay && overlay._id === this._id; + }, this); + }, + + /** + * Creates an instance of a googleMaps Point + */ + _newPoint: function (x, y) { + return new google.maps.Point(x, y); + }, + + _manageOffEvents: function (map, o) { + if (this.options.featureOut) { + return this.options.featureOut && this.options.featureOut(o.e, o.layer); + } + }, + + _manageOnEvents: function (map, o) { + var point = o.pixel; + var latlng = this.projector.pixelToLatLng(point); + var eventType = o.e.type.toLowerCase(); + + switch (eventType) { + case 'mousemove': + if (this.options.featureOver) { + return this.options.featureOver(o.e, latlng, point, o.data, o.layer); + } + break; + + case 'click': + case 'touchend': + case 'touchmove': // for some reason android browser does not send touchend + case 'mspointerup': + case 'pointerup': + case 'pointermove': + if (this.options.featureClick) { + this.options.featureClick(o.e, latlng, point, o.data, o.layer); + } + break; + default: + break; + } + } + } +); + +module.exports = GMapsCartoDBLayerGroupView; diff --git a/src/geo/gmaps/gmaps-layer-view-factory.js b/src/geo/gmaps/gmaps-layer-view-factory.js new file mode 100644 index 0000000..90cb08b --- /dev/null +++ b/src/geo/gmaps/gmaps-layer-view-factory.js @@ -0,0 +1,46 @@ +/* global google */ +var log = require('cdb.log'); + +var GMapsLayerViewFactory = function () {}; + +var constructors = {}; + +// Only "register" the layer view mappings if Google Maps library is present +if (typeof (google) !== 'undefined' && typeof (google.maps) !== 'undefined') { + var GMapsBaseLayerView = require('./gmaps-base-layer-view'); + var GMapsTiledLayerView = require('./gmaps-tiled-layer-view'); + var LeafletWMSLayerView = require('../leaflet/leaflet-wms-layer-view'); + var GMapsPlainLayerView = require('./gmaps-plain-layer-view'); + var GMapsCartoDBLayerGroupView = require('./gmaps-cartodb-layer-group-view'); + var GMapsTorqueLayerView = require('./gmaps-torque-layer-view'); + + constructors = { + 'tiled': GMapsTiledLayerView, + 'wms': LeafletWMSLayerView, + 'plain': GMapsPlainLayerView, + 'gmapsbase': GMapsBaseLayerView, + 'layergroup': GMapsCartoDBLayerGroupView, + 'namedmap': GMapsCartoDBLayerGroupView, + 'torque': GMapsTorqueLayerView + }; +} + +GMapsLayerViewFactory.prototype._constructors = constructors; + +GMapsLayerViewFactory.prototype.createLayerView = function (layerModel, opts) { + var layerType = layerModel.get('type').toLowerCase(); + var LayerViewClass = this._constructors[layerType]; + + if (LayerViewClass) { + try { + return new LayerViewClass(layerModel, opts); + } catch (error) { + log.error("Error creating an instance of layer view for '" + layerType + "' layer -> " + error.message); + throw error; + } + } else { + log.error("Error creating an instance of layer view for '" + layerType + "' layer. Type is not supported"); + } +}; + +module.exports = GMapsLayerViewFactory; diff --git a/src/geo/gmaps/gmaps-layer-view.js b/src/geo/gmaps/gmaps-layer-view.js new file mode 100644 index 0000000..f3055ef --- /dev/null +++ b/src/geo/gmaps/gmaps-layer-view.js @@ -0,0 +1,29 @@ +var _ = require('underscore'); +var Backbone = require('backbone'); + +/** + * base layer for all google maps layers + */ +var GMapsLayerView = function (layerModel, opts) { + opts = opts || {}; + this.model = layerModel; + this.model.bind('change', this._onModelUpdated, this); + this.mapModel = opts.mapModel; + this.gmapsMap = opts.nativeMap; + this.showLimitErrors = opts.showLimitErrors; +}; + +_.extend(GMapsLayerView.prototype, Backbone.Events); +_.extend(GMapsLayerView.prototype, { + addToMap: function () { + throw new Error('Subclasses of GMapsLayerView must implement addToMap'); + }, + + remove: function () { + throw new Error('Subclasses of GMapsLayerView must implement remove'); + }, + + _onModelUpdated: function () {} +}); + +module.exports = GMapsLayerView; diff --git a/src/geo/gmaps/gmaps-map-view.js b/src/geo/gmaps/gmaps-map-view.js new file mode 100644 index 0000000..74455d6 --- /dev/null +++ b/src/geo/gmaps/gmaps-map-view.js @@ -0,0 +1,218 @@ +/* global google */ +var MapView = require('../map-view'); +var Projector = require('./projector'); +var GMapsLayerViewFactory = require('./gmaps-layer-view-factory'); + +var GoogleMapsMapView = MapView.extend({ + initialize: function () { + this._isReady = false; + + MapView.prototype.initialize.apply(this, arguments); + }, + + _createNativeMap: function () { + var self = this; + var center = this.map.get('center'); + + this._gmapsMap = new google.maps.Map(this.el, { + center: new google.maps.LatLng(center[0], center[1]), + zoom: this.map.get('zoom'), + minZoom: this.map.get('minZoom'), + maxZoom: this.map.get('maxZoom'), + disableDefaultUI: true, + scrollwheel: this.map.get('scrollwheel'), + draggable: this.map.get('drag'), + disableDoubleClickZoom: !this.map.get('drag'), + mapTypeControl: false, + mapTypeId: google.maps.MapTypeId.ROADMAP, + backgroundColor: 'white', + tilt: 0 + }); + + this.map.bind('change:maxZoom', function () { + self._gmapsMap.setOptions({ maxZoom: self.map.get('maxZoom') }); + }, this); + + this.map.bind('change:minZoom', function () { + self._gmapsMap.setOptions({ minZoom: self.map.get('minZoom') }); + }, this); + + google.maps.event.addListener(this._gmapsMap, 'center_changed', function () { + var c = self._gmapsMap.getCenter(); + self._setModelProperty({ center: [c.lat(), c.lng()] }); + }); + + google.maps.event.addListener(this._gmapsMap, 'zoom_changed', function () { + self._setModelProperty({ + zoom: self._gmapsMap.getZoom() + }); + var c = self._gmapsMap.getCenter(); + self.map.trigger('moveend', [c.lat(), c.lng()]); + }); + + google.maps.event.addListener(this._gmapsMap, 'click', function (e) { + self.trigger('click', e, [e.latLng.lat(), e.latLng.lng()]); + }); + + google.maps.event.addListener(this._gmapsMap, 'dragend', function (e) { + var c = self._gmapsMap.getCenter(); + self.trigger('dragend', [c.lat(), c.lng()]); + self.map.trigger('moveend', [c.lat(), c.lng()]); + }); + + google.maps.event.addListener(this._gmapsMap, 'dblclick', function (e) { + self.trigger('dblclick', e); + }); + + google.maps.event.addListener(this._gmapsMap, 'bounds_changed', function (e) { + self.map.setMapViewSize(self.getSize()); + }); + + google.maps.event.addListenerOnce(this._gmapsMap, 'idle', function (e) { + self._isReady = true; + }); + + this.projector = new Projector(this._gmapsMap); + }, + + clean: function () { + google.maps.event.clearInstanceListeners(window); + google.maps.event.clearInstanceListeners(document); + + MapView.prototype.clean.call(this); + }, + + /** + * Pass a function to be executed once the map is ready. + */ + onReady: function (callback) { + if (this._isReady) { + callback(); + } else { + google.maps.event.addListenerOnce(this._gmapsMap, 'idle', callback); + } + }, + + _getLayerViewFactory: function () { + this._layerViewFactory = this._layerViewFactory || new GMapsLayerViewFactory(); + + return this._layerViewFactory; + }, + + _setKeyboard: function (model, z) { + this._gmapsMap.setOptions({ keyboardShortcuts: z }); + }, + + _setScrollWheel: function (model, z) { + this._gmapsMap.setOptions({ scrollwheel: z }); + }, + + _setZoom: function (z) { + z = z || 0; + this._gmapsMap.setZoom(z); + }, + + _setCenter: function (center) { + var c = new google.maps.LatLng(center[0], center[1]); + this._gmapsMap.setCenter(c); + }, + + _setView: function () { + if (this.map.hasChanged('zoom')) { + this._setZoom(this.map.get('zoom')); + } + if (this.map.hasChanged('center')) { + this._setCenter(this.map.get('center')); + } + }, + + _getNativeMap: function () { + return this._gmapsMap; + }, + + _addLayerToMap: function (layerView) { + layerView.addToMap(); + }, + + getSize: function () { + return { + x: this.$el.width(), + y: this.$el.height() + }; + }, + + panBy: function (p) { + var c = this.map.get('center'); + var pc = this.latLngToContainerPoint(c); + p.x += pc.x; + p.y += pc.y; + var ll = this.containerPointToLatLng(p); + this.map.setCenter([ll.lat, ll.lng]); + }, + + getBounds: function () { + if (this._isReady) { + var b = this._gmapsMap.getBounds(); + var sw = b.getSouthWest(); + var ne = b.getNorthEast(); + return [ + [sw.lat(), sw.lng()], + [ne.lat(), ne.lng()] + ]; + } + return [[0, 0], [0, 0]]; + }, + + setCursor: function (cursor) { + this._gmapsMap.setOptions({ draggableCursor: cursor }); + }, + + getNativeMap: function () { + return this._gmapsMap; + }, + + invalidateSize: function () { + google.maps.event.trigger(this._gmapsMap, 'resize'); + this.map.setMapViewSize(this.getSize()); + }, + + // GEOMETRY + + addMarker: function (marker) { + marker.addToMap(this.getNativeMap()); + }, + + removeMarker: function (marker) { + marker.removeFromMap(this.getNativeMap()); + }, + + hasMarker: function (marker) { + return marker.isAddedToMap(this.getNativeMap()); + }, + + addPath: function (path) { + path.addToMap(this.getNativeMap()); + }, + + removePath: function (path) { + path.removeFromMap(this.getNativeMap()); + }, + + latLngToContainerPoint: function (latlng) { + var point = this.projector.latLngToPixel(new google.maps.LatLng(latlng[0], latlng[1])); + return { + x: point.x, + y: point.y + }; + }, + + containerPointToLatLng: function (point) { + var latlng = this.projector.pixelToLatLng(new google.maps.Point(point.x, point.y)); + return { + lat: latlng.lat(), + lng: latlng.lng() + }; + } +}); + +module.exports = GoogleMapsMapView; diff --git a/src/geo/gmaps/gmaps-plain-layer-view.js b/src/geo/gmaps/gmaps-plain-layer-view.js new file mode 100644 index 0000000..2bfec79 --- /dev/null +++ b/src/geo/gmaps/gmaps-plain-layer-view.js @@ -0,0 +1,34 @@ +/* global google */ +var _ = require('underscore'); +var GMapsLayerView = require('./gmaps-layer-view'); + +var GMapsPlainLayerView = function (layerModel, opts) { + this.color = layerModel.get('color'); + GMapsLayerView.apply(this, arguments); +}; + +_.extend( + GMapsPlainLayerView.prototype, + GMapsLayerView.prototype, { + + _update: function () { + this.color = this.model.get('color'); + this.refreshView(); + }, + + getTile: function (coord, zoom, ownerDocument) { + var div = document.createElement('div'); + div.style.width = this.tileSize.x; + div.style.height = this.tileSize.y; + div['background-color'] = this.color; + return div; + }, + + tileSize: new google.maps.Size(256, 256), + maxZoom: 100, + minZoom: 0, + name: 'plain layer', + alt: 'plain layer' + }); + +module.exports = GMapsPlainLayerView; diff --git a/src/geo/gmaps/gmaps-tiled-layer-view.js b/src/geo/gmaps/gmaps-tiled-layer-view.js new file mode 100644 index 0000000..1c6a146 --- /dev/null +++ b/src/geo/gmaps/gmaps-tiled-layer-view.js @@ -0,0 +1,42 @@ +/* global google */ +var _ = require('underscore'); +var GMapsLayerView = require('./gmaps-layer-view'); + +var GMapsTiledLayerView = function (layerModel, opts) { + GMapsLayerView.apply(this, arguments); + this.tileSize = new google.maps.Size(256, 256); + this.opacity = 1.0; + this.isPng = true; + this.maxZoom = 22; + this.minZoom = 0; + this.name = 'cartodb tiled layer'; + google.maps.ImageMapType.call(this, this); +}; + +_.extend( + GMapsTiledLayerView.prototype, + GMapsLayerView.prototype, + google.maps.ImageMapType.prototype, { + + getTileUrl: function (tile, zoom) { + var y = tile.y; + var tileRange = 1 << zoom; + if (y < 0 || y >= tileRange) { + return null; + } + var x = tile.x; + if (x < 0 || x >= tileRange) { + x = (x % tileRange + tileRange) % tileRange; + } + if (this.model.get('tms')) { + y = tileRange - y - 1; + } + var urlPattern = this.model.get('urlTemplate'); + return urlPattern + .replace('{x}', x) + .replace('{y}', y) + .replace('{z}', zoom); + } + }); + +module.exports = GMapsTiledLayerView; diff --git a/src/geo/gmaps/gmaps-torque-layer-view.js b/src/geo/gmaps/gmaps-torque-layer-view.js new file mode 100644 index 0000000..13a684e --- /dev/null +++ b/src/geo/gmaps/gmaps-torque-layer-view.js @@ -0,0 +1,45 @@ +require('torque.js'); +var _ = require('underscore'); +var torque = require('torque.js'); +var Backbone = require('backbone'); +var GMapsLayerView = require('./gmaps-layer-view'); +var TorqueLayerViewBase = require('../torque-layer-view-base'); + +var GMapsTorqueLayerView = function (layerModel, opts) { + GMapsLayerView.apply(this, arguments); + + torque.GMapsTorqueLayer.call(this, this._initialAttrs(layerModel)); + + // TODO: revisit this and use composition like we're doing with Leaflet + this.setNativeTorqueLayer(this); +}; + +_.extend( + GMapsTorqueLayerView.prototype, + GMapsLayerView.prototype, + torque.GMapsTorqueLayer.prototype, + TorqueLayerViewBase, + { + addToMap: function () { + this.setMap(this.gmapsMap); + }, + + remove: function () { + this.setMap(null); + }, + + onAdd: function () { + torque.GMapsTorqueLayer.prototype.onAdd.apply(this); + }, + + onTilesLoaded: function () { + // this.trigger('load'); + Backbone.Events.trigger.call(this, 'load'); + }, + + onTilesLoading: function () { + Backbone.Events.trigger.call(this, 'loading'); + } + }); + +module.exports = GMapsTorqueLayerView; diff --git a/src/geo/gmaps/projector.js b/src/geo/gmaps/projector.js new file mode 100644 index 0000000..3627f8c --- /dev/null +++ b/src/geo/gmaps/projector.js @@ -0,0 +1,27 @@ +/* global google */ +// helper to get pixel position from latlon + +function Projector (map) { + this.setMap(map); +} +Projector.prototype = new google.maps.OverlayView(); +Projector.prototype.draw = function () {}; +Projector.prototype.latLngToPixel = function (latlng) { + var projection = this.getProjection(); + if (projection) { + return projection.fromLatLngToContainerPixel(latlng); + } + console.warn('Projector has no projection'); + return new google.maps.Point(0, 0); +}; + +Projector.prototype.pixelToLatLng = function (point) { + var projection = this.getProjection(); + if (projection) { + return projection.fromContainerPixelToLatLng(point); + } + console.warn('Projector has no projection'); + return new google.maps.LatLng(0, 0); +}; + +module.exports = Projector; diff --git a/src/geo/leaflet/leaflet-cartodb-layer-group-view.js b/src/geo/leaflet/leaflet-cartodb-layer-group-view.js new file mode 100644 index 0000000..41de4a4 --- /dev/null +++ b/src/geo/leaflet/leaflet-cartodb-layer-group-view.js @@ -0,0 +1,126 @@ +/* global L */ +var _ = require('underscore'); +var C = require('../../constants'); +var LeafletLayerView = require('./leaflet-layer-view'); +var CartoDBLayerGroupViewBase = require('../cartodb-layer-group-view-base'); +var zera = require('@carto/zera'); +var EMPTY_GIF = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; + +var LeafletCartoDBLayerGroupView = function (layerModel, opts) { + LeafletLayerView.apply(this, arguments); + CartoDBLayerGroupViewBase.apply(this, arguments); + + this.leafletLayer.on('load', function () { + this.trigger('load'); + }.bind(this)); + + this.leafletLayer.on('loading', function () { + this.trigger('loading'); + }.bind(this)); + + this.leafletLayer.on('tileerror', function (layer) { + this.model.addError({ type: C.WINDSHAFT_ERRORS.TILE }); + }.bind(this)); +}; + +LeafletCartoDBLayerGroupView.prototype = _.extend( + {}, + LeafletLayerView.prototype, + CartoDBLayerGroupViewBase.prototype, + { + interactionClass: zera.Interactive, + + _createLeafletLayer: function () { + var tileLayer = new L.TileLayer(null, { + opacity: 0.99, + maxZoom: 30 + }); + tileLayer._setUrl = function (url, noDraw) { + return L.TileLayer.prototype.setUrl.call(this, url, noDraw); + }; + return tileLayer; + }, + + _reload: function () { + var tileURLTemplate = this.model.getTileURLTemplate(); + var subdomains = this.model.getSubdomains(); + + if (!tileURLTemplate) { + tileURLTemplate = EMPTY_GIF; + } + + if (subdomains) { + L.Util.setOptions(this.leafletLayer, { subdomains: subdomains }); + } + + this.leafletLayer._setUrl(tileURLTemplate); + + this._reloadInteraction(); + }, + + _manageOffEvents: function (nativeMap, zeraEvent) { + this._onFeatureOut(zeraEvent.layer); + }, + + _manageOnEvents: function (nativeMap, zeraEvent) { + var containerPoint = nativeMap.layerPointToContainerPoint(zeraEvent.layerPoint); + + if (!containerPoint || isNaN(containerPoint.x) || isNaN(containerPoint.y)) { + return false; + } + + var latlng = nativeMap.containerPointToLatLng(containerPoint); + + var eventType = zeraEvent.e.type.toLowerCase(); + + switch (eventType) { + case 'mousemove': + this._onFeatureOver(latlng, containerPoint, zeraEvent.data, zeraEvent.layer); + break; + case 'click': + this._onFeatureClicked(latlng, containerPoint, zeraEvent.data, zeraEvent.layer); + break; + } + }, + + _onFeatureClicked: function (latlon, containerPoint, data, layer) { + var layerModel = this.model.getLayerInLayerGroupAt(layer); + if (layerModel) { + this.trigger('featureClick', { + layer: layerModel, + layerIndex: layer, + latlng: [latlon.lat, latlon.lng], + position: containerPoint, + feature: data + }); + } + }, + + _onFeatureOver: function (latlon, containerPoint, data, layer) { + var layerModel = this.model.getLayerInLayerGroupAt(layer); + if (layerModel) { + this.trigger('featureOver', { + layer: layerModel, + layerIndex: layer, + latlng: [latlon.lat, latlon.lng], + position: containerPoint, + feature: data + }); + } + }, + + _onFeatureOut: function (layerIndex) { + var layerModel = this.model.getLayerInLayerGroupAt(layerIndex); + if (layerModel) { + this.trigger('featureOut', { + layer: layerModel, + layerIndex: layerIndex + }); + } + } + } +); + +LeafletCartoDBLayerGroupView.prototype.constructor = LeafletLayerView; + +module.exports = LeafletCartoDBLayerGroupView; diff --git a/src/geo/leaflet/leaflet-layer-view-factory.js b/src/geo/leaflet/leaflet-layer-view-factory.js new file mode 100644 index 0000000..7d10fc8 --- /dev/null +++ b/src/geo/leaflet/leaflet-layer-view-factory.js @@ -0,0 +1,43 @@ +var log = require('cdb.log'); +var LeafletTiledLayerView = require('./leaflet-tiled-layer-view'); +var LeafletWMSLayerView = require('./leaflet-wms-layer-view'); +var LeafletPlainLayerView = require('./leaflet-plain-layer-view'); +var LeafletCartoDBLayerGroupView = require('./leaflet-cartodb-layer-group-view'); +var LeafletTorqueLayerView = require('./leaflet-torque-layer-view'); +var RenderModes = require('../../geo/render-modes'); + +var LayerGroupViewConstructor = function (layerGroupModel, opts) { + opts = opts || {}; + if (opts.mapModel.get('renderMode') === RenderModes.VECTOR) { + console.warn('Vector rendering is not supported anymore'); + } + return new LeafletCartoDBLayerGroupView(layerGroupModel, opts); +}; + +var LeafletLayerViewFactory = function () { }; + +LeafletLayerViewFactory.prototype._constructors = { + 'tiled': LeafletTiledLayerView, + 'wms': LeafletWMSLayerView, + 'plain': LeafletPlainLayerView, + 'layergroup': LayerGroupViewConstructor, + 'torque': LeafletTorqueLayerView +}; + +LeafletLayerViewFactory.prototype.createLayerView = function (layerModel, opts) { + var layerType = layerModel.get('type').toLowerCase(); + var LayerViewClass = this._constructors[layerType]; + + if (LayerViewClass) { + try { + return new LayerViewClass(layerModel, opts); + } catch (error) { + log.error("Error creating an instance of layer view for '" + layerType + "' layer -> " + error.message); + throw error; + } + } else { + log.error("Error creating an instance of layer view for '" + layerType + "' layer. Type is not supported"); + } +}; + +module.exports = LeafletLayerViewFactory; diff --git a/src/geo/leaflet/leaflet-layer-view.js b/src/geo/leaflet/leaflet-layer-view.js new file mode 100644 index 0000000..abb7e12 --- /dev/null +++ b/src/geo/leaflet/leaflet-layer-view.js @@ -0,0 +1,53 @@ +var _ = require('underscore'); +var Backbone = require('backbone'); + +var LeafletLayerView = function (layerModel, opts) { + opts = opts || {}; + this.model = layerModel; + this.mapModel = opts.mapModel; + this.leafletMap = opts.nativeMap; + this.leafletLayer = opts.nativeLayer || this._createLeafletLayer(); + this.showLimitErrors = opts.showLimitErrors; + + this.setModel(layerModel); + + var type = layerModel.get('type') || layerModel.get('kind'); + this.type = type && type.toLowerCase(); +}; + +_.extend(LeafletLayerView.prototype, Backbone.Events); +_.extend(LeafletLayerView.prototype, { + + setZIndex: function (index) { + this.leafletLayer.setZIndex(index); + }, + + setModel: function (model) { + if (this.model) { + this.model.unbind('change', this._modelUpdated, this); + } + this.model = model; + this.model.bind('change', this._modelUpdated, this); + }, + + remove: function () { + this.leafletMap.removeLayer(this.leafletLayer); + this.notifyRemove(); + }, + + notifyRemove: function () { + this.trigger('remove', this); + this.model.unbind(null, null, this); + this.unbind(); + }, + + reload: function () { + this.leafletLayer.redraw(); + }, + + _createLeafletLayer: function () { + throw new Error('subclasses of LeafletLayerView must implement _createLeafletLayer'); + } +}); + +module.exports = LeafletLayerView; diff --git a/src/geo/leaflet/leaflet-map-view.js b/src/geo/leaflet/leaflet-map-view.js new file mode 100644 index 0000000..0269dc7 --- /dev/null +++ b/src/geo/leaflet/leaflet-map-view.js @@ -0,0 +1,301 @@ +/* global L */ +var $ = require('jquery'); +var _ = require('underscore'); +var MapView = require('../map-view'); +var LeafletLayerViewFactory = require('./leaflet-layer-view-factory'); + +var LeafletMapView = MapView.extend({ + _createNativeMap: function () { + var self = this; + var center = this.map.get('center'); + + var mapConfig = { + zoomControl: false, + center: new L.LatLng(center[0], center[1]), + zoom: this.map.get('zoom'), + minZoom: this.map.get('minZoom'), + maxZoom: this.map.get('maxZoom'), + dragging: !!this.map.get('drag'), + doubleClickZoom: !!this.map.get('drag'), + scrollWheelZoom: !!this.map.get('scrollwheel'), + keyboard: !!this.map.get('keyboard'), + attributionControl: false, + zoomAnimationThreshold: 1000 + }; + + this._leafletMap = new L.Map(this.el, mapConfig); + + this.map.bind('set_view', this._setView, this); + + this._leafletMap.on('layeradd', function (lyr) { + this.trigger('layeradd', lyr, self); + }, this); + + this._leafletMap.on('zoomstart', function () { + self.trigger('zoomstart'); + }); + + this._leafletMap.on('click', function (e) { + self.trigger('click', e.originalEvent, [e.latlng.lat, e.latlng.lng]); + }); + + this._leafletMap.on('dblclick', function (e) { + self.trigger('dblclick', e.originalEvent); + }); + + this._leafletMap.on('zoomend', function () { + self._setModelProperty({ + zoom: self._leafletMap.getZoom() + }); + self.trigger('zoomend'); + }, this); + + this._leafletMap.on('dragend', function () { + var c = self._leafletMap.getCenter(); + this.trigger('dragend', [c.lat, c.lng]); + }, this); + + this._leafletMap.on('moveend', function () { + var c = self._leafletMap.getCenter(); + self._setModelProperty({ + center: [c.lat, c.lng] + }); + self.map.trigger('moveend', [c.lat, c.lng]); + }, this); + + this._leafletMap.on('drag', function () { + var c = self._leafletMap.getCenter(); + self._setModelProperty({ + center: [c.lat, c.lng] + }); + self.trigger('drag'); + }, this); + + this._leafletMap.on('resize', function () { + this.map.setMapViewSize(this.getSize()); + }, this); + + this.map.bind('change:maxZoom', function () { + L.Util.setOptions(self._leafletMap, { maxZoom: self.map.get('maxZoom') }); + }, this); + + this.map.bind('change:minZoom', function () { + L.Util.setOptions(self._leafletMap, { minZoom: self.map.get('minZoom') }); + }, this); + }, + + _getLayerViewFactory: function () { + this._layerViewFactory = this._layerViewFactory || new LeafletLayerViewFactory(); + + return this._layerViewFactory; + }, + + // this replaces the default functionality to search for + // already added views so they are not replaced + _addLayers: function (layerCollection, options) { + var self = this; + + var oldLayers = this._layerViews; + this._layerViews = {}; + + function findLayerView (layer) { + var lv = _.find(oldLayers, function (layerView) { + var m = layerView.model; + return m.isEqual(layer); + }); + return lv; + } + + function canReused (layer) { + return self.map.layers.find(function (m) { + return m.isEqual(layer); + }); + } + + // remove all + for (var layer in oldLayers) { + var layerView = oldLayers[layer]; + if (!canReused(layerView.model)) { + layerView.remove(); + } + } + + this.map.layers.each(function (layerModel) { + var layerView = findLayerView(layerModel); + if (!layerView) { + self._addLayer(layerModel, layerCollection, { + silent: (options && options.silent) || false, + index: options && options.index + }); + } else { + layerView.setModel(layerModel); + self._layerViews[layerModel.cid] = layerView; + } + }); + }, + + clean: function () { + // see https://github.com/CloudMade/Leaflet/issues/1101 + L.DomEvent.off(window, 'resize', this._leafletMap._onResize, this._leafletMap); + + // destroy the map and clear all related event listeners + this._leafletMap.remove(); + + MapView.prototype.clean.call(this); + }, + + _setKeyboard: function (model, z) { + if (z) { + this._leafletMap.keyboard.enable(); + } else { + this._leafletMap.keyboard.disable(); + } + }, + + _setScrollWheel: function (model, z) { + if (z) { + this._leafletMap.scrollWheelZoom.enable(); + } else { + this._leafletMap.scrollWheelZoom.disable(); + } + }, + + _setZoom: function (model, z) { + this._setView(); + }, + + _setCenter: function (model, center) { + this._setView(); + }, + + _setView: function () { + if (this.map.hasChanged('zoom') || this.map.hasChanged('center')) { + this._leafletMap.setView(this.map.get('center'), this.map.get('zoom') || 0); + } + }, + + _getNativeMap: function () { + return this._leafletMap; + }, + + _addLayerToMap: function (layerView) { + this._leafletMap.addLayer(layerView.leafletLayer); + this._reorderLayerViews(); + }, + + _reorderLayerViews: function () { + this.map.layers.each(function (layerModel) { + var layerView = this.getLayerViewByLayerCid(layerModel.cid); + + // CartoDBLayers share the same layerView so the zIndex is being overriden on every iteration. + // The layerView will get the order of the last CartoDB layer as the zIndex + if (layerView) { + layerView.setZIndex(layerModel.get('order')); + } + }, this); + }, + + /** + * Pass a function to be executed once the map is ready. + */ + onReady: function (callback) { + // A Leaflet maps is always ready + callback(); + }, + + // return the current bounds of the map view + getBounds: function () { + var b = this._leafletMap.getBounds(); + var sw = b.getSouthWest(); + var ne = b.getNorthEast(); + return [ + [sw.lat, sw.lng], + [ne.lat, ne.lng] + ]; + }, + + getSize: function () { + return this._leafletMap.getSize(); + }, + + panBy: function (p) { + this._leafletMap.panBy(new L.Point(p.x, p.y)); + }, + + setCursor: function (cursor) { + $(this._leafletMap.getContainer()).css('cursor', cursor); + }, + + getNativeMap: function () { + return this._leafletMap; + }, + + invalidateSize: function () { + var center = this.map.get('center'); + var zoom = this.map.get('zoom'); + this._leafletMap.invalidateSize({ pan: false, animate: false }); + this._leafletMap.setView(center, zoom, { pan: false, animate: false }); + this.map.setMapViewSize(this.getSize()); + }, + + // GEOMETRY + + addMarker: function (marker) { + marker.addToMap(this.getNativeMap()); + }, + + removeMarker: function (marker) { + marker.removeFromMap(this.getNativeMap()); + }, + + hasMarker: function (marker) { + return marker.isAddedToMap(this.getNativeMap()); + }, + + addPath: function (path) { + path.addToMap(this.getNativeMap()); + }, + + removePath: function (path) { + path.removeFromMap(this.getNativeMap()); + }, + + latLngToContainerPoint: function (latlng) { + var point = this.getNativeMap().latLngToContainerPoint(latlng); + return { + x: point.x, + y: point.y + }; + }, + + // returns { lat: 0, lng: 0} + containerPointToLatLng: function (point) { + var latlng = this.getNativeMap().containerPointToLatLng([point.x, point.y]); + return { + lat: latlng.lat, + lng: latlng.lng + }; + } +}); + +// set the image path in order to be able to get leaflet icons +// code adapted from leaflet +L.Icon.Default.imagePath = (function () { + var scripts = document.getElementsByTagName('script'); + var leafletRe = /\/?cartodb[\-\._]?([\w\-\._]*)\.js\??/; + + var i, len, src, matches; + + for (i = 0, len = scripts.length; i < len; i++) { + src = scripts[i].src; + matches = src.match(leafletRe); + + if (matches) { + var bits = src.split('/'); + delete bits[bits.length - 1]; + return bits.join('/') + 'themes/css/images'; + } + } +}()); + +module.exports = LeafletMapView; diff --git a/src/geo/leaflet/leaflet-plain-layer-view.js b/src/geo/leaflet/leaflet-plain-layer-view.js new file mode 100644 index 0000000..f3a03ed --- /dev/null +++ b/src/geo/leaflet/leaflet-plain-layer-view.js @@ -0,0 +1,46 @@ +/* global L */ +var _ = require('underscore'); +var LeafletLayerView = require('./leaflet-layer-view'); + +var LeafletPlainLayerView = function (layerModel, opts) { + LeafletLayerView.apply(this, arguments); +}; + +LeafletPlainLayerView.prototype = _.extend( + {}, + LeafletLayerView.prototype, + { + _createLeafletLayer: function () { + var leafletLayer = new L.Layer(); + + leafletLayer.onAdd = function () { + this._redraw(); + }.bind(this); + + leafletLayer.onRemove = function () { + var div = this.leafletMap.getContainer(); + div.style.background = 'none'; + }.bind(this); + + leafletLayer.setZIndex = function () {}; + + return leafletLayer; + }, + + _modelUpdated: function () { + this._redraw(); + }, + + _redraw: function () { + var div = this.leafletMap.getContainer(); + div.style.backgroundColor = this.model.get('color') || '#FFF'; + + if (this.model.get('image')) { + var style = 'transparent url(' + this.model.get('image') + ') repeat center center'; + div.style.background = style; + } + } + } +); + +module.exports = LeafletPlainLayerView; diff --git a/src/geo/leaflet/leaflet-tiled-layer-view.js b/src/geo/leaflet/leaflet-tiled-layer-view.js new file mode 100644 index 0000000..5970d74 --- /dev/null +++ b/src/geo/leaflet/leaflet-tiled-layer-view.js @@ -0,0 +1,76 @@ +/* global L */ +var _ = require('underscore'); +var LeafletLayerView = require('./leaflet-layer-view'); + +var LeafletTiledLayerView = function (layerModel, opts) { + LeafletLayerView.apply(this, arguments); + + this.leafletLayer.on('load', function (e) { + this.trigger('load'); + }.bind(this)); + + this.leafletLayer.on('loading', function (e) { + this.trigger('loading'); + }.bind(this)); + + var self = this; + this.leafletLayer.onAdd = function (map) { + L.TileLayer.prototype.onAdd.apply(this, arguments); + self._onAdd(); + }; +}; + +LeafletTiledLayerView.prototype = _.extend( + {}, + LeafletLayerView.prototype, + { + _createLeafletLayer: function () { + return new L.TileLayer(_getUrlTemplate(this._isHdpi(), this.model), _generateLeafletLayerOptions(this.model)); + }, + + _onAdd: function () { + var container = this.leafletLayer.getContainer(); + // Disable mouse events for the container of this layer so that + // events are not captured and other layers below can respond to mouse + // events + container.style.pointerEvents = 'none'; + }, + + _modelUpdated: function () { + L.Util.setOptions(this.leafletLayer, _generateLeafletLayerOptions(this.model)); + this.leafletLayer.setUrl(_getUrlTemplate(this._isHdpi(), this.model)); + }, + + _isHdpi: function () { + return window && window.devicePixelRatio && window.devicePixelRatio > 1; + } + } +); + +LeafletTiledLayerView.prototype.constructor = LeafletTiledLayerView; + +var _generateLeafletLayerOptions = function generateLeafletLayerOptions (layerModel) { + return { + tms: !!layerModel.get('tms'), + attribution: layerModel.get('attribution'), + minZoom: layerModel.get('minZoom'), + maxZoom: layerModel.get('maxZoom'), + subdomains: layerModel.get('subdomains') || 'abc', + errorTileUrl: layerModel.get('errorTileUrl'), + opacity: layerModel.get('opacity') + }; +}; + +var _getUrlTemplate = function getUrlTemplate (isHdpi, layerModel) { + var changedAttributes = layerModel.changedAttributes(); + + if (changedAttributes.urlTemplate && !changedAttributes.urlTemplate2x) { + layerModel.unset('urlTemplate2x', { silent: true }); + } + + return isHdpi && layerModel.get('urlTemplate2x') + ? layerModel.get('urlTemplate2x') + : layerModel.get('urlTemplate'); +}; + +module.exports = LeafletTiledLayerView; diff --git a/src/geo/leaflet/leaflet-torque-layer-view.js b/src/geo/leaflet/leaflet-torque-layer-view.js new file mode 100644 index 0000000..cebd9fc --- /dev/null +++ b/src/geo/leaflet/leaflet-torque-layer-view.js @@ -0,0 +1,36 @@ +/* global L */ +require('torque.js'); +var _ = require('underscore'); +var LeafletLayerView = require('./leaflet-layer-view'); +var TorqueLayerViewBase = require('../torque-layer-view-base'); +var util = require('cdb.core.util'); + +var LeafletTorqueLayer = function (layerModel, opts) { + LeafletLayerView.apply(this, arguments); + this.setNativeTorqueLayer(this.leafletLayer); +}; + +LeafletTorqueLayer.prototype = _.extend( + {}, + LeafletLayerView.prototype, + TorqueLayerViewBase, + { + _createLeafletLayer: function () { + var query = this._getQuery(this.model); + var attrs = this._initialAttrs(this.model); + + _.extend(attrs, { + dynamic_cdn: this.model.get('dynamic_cdn'), + showLimitErrors: this.showLimitErrors, + instanciateCallback: function () { + var cartocss = this.model.get('cartocss') || this.model.get('tile_style'); + return '_cdbct_' + util.uniqueCallbackName(cartocss + query); + }.bind(this) + }); + + return new L.TorqueLayer(attrs); + } + } +); + +module.exports = LeafletTorqueLayer; diff --git a/src/geo/leaflet/leaflet-wms-layer-view.js b/src/geo/leaflet/leaflet-wms-layer-view.js new file mode 100644 index 0000000..b846ff1 --- /dev/null +++ b/src/geo/leaflet/leaflet-wms-layer-view.js @@ -0,0 +1,46 @@ +/* global L */ +var _ = require('underscore'); +var LeafletLayerView = require('./leaflet-layer-view.js'); + +var generateLeafletLayerOptions = function (layerModel) { + return { + attribution: layerModel.get('attribution'), + layers: layerModel.get('layers'), + format: layerModel.get('format'), + transparent: layerModel.get('transparent'), + minZoom: layerModel.get('minZomm'), + maxZoom: layerModel.get('maxZoom'), + subdomains: layerModel.get('subdomains') || 'abc', + errorTileUrl: layerModel.get('errorTileUrl'), + opacity: layerModel.get('opacity') + }; +}; + +var LeafletWMSLayerView = function (layerModel, opts) { + LeafletLayerView.apply(this, arguments); + + this.leafletLayer.on('load', function (e) { + this.trigger('load'); + }.bind(this)); + + this.leafletLayer.on('loading', function (e) { + this.trigger('loading'); + }.bind(this)); +}; + +LeafletWMSLayerView.prototype = _.extend( + {}, + LeafletLayerView.prototype, + { + _createLeafletLayer: function () { + return new L.TileLayer.WMS(this.model.get('urlTemplate'), generateLeafletLayerOptions(this.model)); + }, + + _modelUpdated: function () { + L.Util.setOptions(this.leafletLayer, generateLeafletLayerOptions(this.model)); + this.leafletLayer.setUrl(this.model.get('urlTemplate')); + } + } +); + +module.exports = LeafletWMSLayerView; diff --git a/src/geo/map-view-factory.js b/src/geo/map-view-factory.js new file mode 100644 index 0000000..e80f693 --- /dev/null +++ b/src/geo/map-view-factory.js @@ -0,0 +1,48 @@ +var LeafletMapView = require('./leaflet/leaflet-map-view'); +var GoogleMapsMapView; +// Check if google maps is defined +if (window.google && window.google.maps) { + GoogleMapsMapView = require('./gmaps/gmaps-map-view'); +} + +/** + * Create a map view. + */ +function MapViewFactory () { + +} + +MapViewFactory.createMapView = function (provider, visModel) { + switch (provider) { + case 'leaflet': + return _createLeafletMap(visModel); + case 'googlemaps': + return _createGoogleMap(visModel); + + default: + throw new Error(provider + ' provider is not supported'); + } +}; + +function _createLeafletMap (visModel) { + return new LeafletMapView(_generateOptions(visModel)); +} + +function _createGoogleMap (visModel) { + if (!GoogleMapsMapView) { + throw new Error('Google maps library should be included'); + } + return new GoogleMapsMapView(_generateOptions(visModel)); +} + +function _generateOptions (visModel) { + return { + showEmptyInfowindowFields: visModel.get('showEmptyInfowindowFields'), + showLimitErrors: visModel.get('showLimitErrors'), + mapModel: visModel.map, + engine: visModel.getEngine(), + layerGroupModel: visModel.getEngine().getLayerGroup() + }; +} + +module.exports = MapViewFactory; diff --git a/src/geo/map-view.js b/src/geo/map-view.js new file mode 100644 index 0000000..3941f3e --- /dev/null +++ b/src/geo/map-view.js @@ -0,0 +1,362 @@ +var _ = require('underscore'); +var log = require('cdb.log'); +var View = require('../core/view'); +var GeometryViewFactory = require('./geometry-views/geometry-view-factory'); + +var InfowindowModel = require('./ui/infowindow-model'); +var InfowindowView = require('./ui/infowindow-view'); +var InfowindowManager = require('../vis/infowindow-manager'); + +var TooltipModel = require('./ui/tooltip-model'); +var TooltipView = require('./ui/tooltip-view'); +var TooltipManager = require('../vis/tooltip-manager'); + +var MapCursorManager = require('../vis/map-cursor-manager'); +var MapEventsManager = require('../vis/map-events-manager'); +var GeometryManagementController = require('../vis/geometry-management-controller'); + +var MapView = View.extend({ + + className: 'CDB-Map-wrapper', + + initialize: function (opts) { + View.prototype.initialize.apply(this, arguments); + + // For debugging purposes + window.mapView = this; + + if (!opts.mapModel) throw new Error('mapModel is required'); + if (!opts.engine) throw new Error('engine is required'); + if (!opts.layerGroupModel) throw new Error('layerGroupModel is required'); + + this._showEmptyInfowindowFields = opts.showEmptyInfowindowFields; + this._showLimitErrors = opts.showLimitErrors; + this._mapModel = this.map = opts.mapModel; + this._engine = opts.engine; + this._cartoDBLayerGroup = opts.layerGroupModel; + + this.add_related_model(this.map); + + this._cartoDBLayerGroupView = null; + + // A map of the LayerViews that is linked to each of the Layer models. + // The cid of the layer model is used as the key for this mapping. + this._layerViews = {}; + + this._bindModel(); + + this.map.layers.bind('reset', this._addLayers, this); + this.map.layers.bind('add', this._addLayer, this); + this.map.layers.bind('remove', this._removeLayer, this); + this.add_related_model(this.map.layers); + + this.map.geometries.on('add', this._onGeometryAdded, this); + this.add_related_model(this.map.geometries); + + // Infowindows && Tooltips + var infowindowModel = new InfowindowModel(); + var tooltipModel = new TooltipModel({ + offset: [4, 10] + }); + this._infowindowView = new InfowindowView({ + model: infowindowModel, + mapView: this + }); + this._tooltipView = new TooltipView({ + model: tooltipModel, + mapView: this + }); + + // Initialise managers + this._infowindowManager = new InfowindowManager({ + engine: this._engine, + mapModel: this._mapModel, + tooltipModel: tooltipModel, + infowindowModel: infowindowModel + }, { + + showEmptyFields: this._showEmptyInfowindowFields + }); + this._tooltipManager = new TooltipManager({ + mapModel: this._mapModel, + tooltipModel: tooltipModel, + infowindowModel: infowindowModel + }); + this._geometryManagementController = new GeometryManagementController(this, this._mapModel); + this._mapCursorManager = new MapCursorManager({ + mapView: this, + mapModel: this._mapModel + }); + + this._mapEventsManager = new MapEventsManager({ + mapModel: this._mapModel + }); + + this._linkMapHelpers(); + }, + + clean: function () { + this._removeLayers(); + + delete this._cartoDBLayerGroupView; + + // Clean Infowindow and Tooltip views + this._infowindowView.clean(); + this._tooltipView.clean(); + + // Stop managers + this._geometryManagementController.stop(); + this._infowindowManager.stop(); + this._tooltipManager.stop(); + this._mapCursorManager.stop(); + this._mapEventsManager.stop(); + + this._unbindModel(); + + View.prototype.clean.call(this); + }, + + render: function () { + this._createNativeMap(); + this._addLayers(); + + // Enable geometry management + this._geometryManagementController.start(); + this.map.setMapViewSize(this.getSize()); + return this; + }, + + _linkMapHelpers: function () { + this.map.setPixelToLatLngConverter(this.containerPointToLatLng.bind(this)); + this.map.setLatLngToPixelConverter(this.latLngToContainerPoint.bind(this)); + }, + + _onGeometryAdded: function (geometry) { + var geometryView = GeometryViewFactory.createGeometryView(this.map.get('provider'), geometry, this); + geometryView.render(); + }, + + /** + * set model property but unbind changes first in order to not create an infinite loop + */ + _setModelProperty: function (prop) { + this._unbindModel(); + this.map.set(prop); + if (prop.center !== undefined || prop.zoom !== undefined) { + var b = this.getBounds(); + this.map.set({ + view_bounds_sw: b[0], + view_bounds_ne: b[1] + }); + } + this._bindModel(); + }, + + /** bind model properties */ + _bindModel: function () { + this._unbindModel(); + this.map.bind('change:view_bounds_sw', this._changeBounds, this); + this.map.bind('change:view_bounds_ne', this._changeBounds, this); + this.map.bind('change', this._setView, this); + this.map.bind('change:scrollwheel', this._setScrollWheel, this); + this.map.bind('change:keyboard', this._setKeyboard, this); + }, + + /** unbind model properties */ + _unbindModel: function () { + this.map.unbind('change:view_bounds_sw', this._changeBounds, this); + this.map.unbind('change:view_bounds_ne', this._changeBounds, this); + this.map.unbind('change', this._setView, this); + this.map.unbind('change:scrollwheel', this._setScrollWheel, this); + this.map.unbind('change:keyboard', this._setKeyboard, this); + this.map.unbind('change:attribution', null, this); + }, + + _changeBounds: function () { + var bounds = this.map.getViewBounds(); + if (bounds) { + this._fitBounds(bounds); + } + }, + + _fitBounds: function (bounds) { + this.map.fitBounds(bounds, this.getSize()); + }, + + _addLayers: function (layerCollection, options) { + var self = this; + this._removeLayers(); + this.map.layers.each(function (layerModel, index) { + self._addLayer(layerModel, layerCollection, { + silent: (options && options.silent) || false, + index: index + }); + }); + }, + + _addLayer: function (layerModel, layerCollection, options) { + options = options || {}; + + var layerView; + if (layerModel.get('type') === 'CartoDB') { + layerView = this._addGroupedLayer(layerModel); + } else { + layerView = this._addIndividualLayer(layerModel); + } + + if (!layerView) { + return; + } + this._addLayerToMap(layerView); + + if (!options.silent) { + this.trigger('newLayerView', layerView); + } + }, + + _addGroupedLayer: function (layerModel) { + // Layer group view already exists + if (this._cartoDBLayerGroupView) { + this._layerViews[layerModel.cid] = this._cartoDBLayerGroupView; + return; + } + + // Create the view that groups CartoDB layers + this._cartoDBLayerGroupView = this._createLayerView(this._cartoDBLayerGroup); + this._layerViews[layerModel.cid] = this._cartoDBLayerGroupView; + + // Render the infowindow and tooltip "overlays" + this._infowindowView.render(); + this.$el.append(this._infowindowView.el); + this._tooltipView.render(); + this.$el.append(this._tooltipView.el); + + // Start managers that should be bound to the CartoDB layer group view + this._infowindowManager.start(this._cartoDBLayerGroupView); + this._tooltipManager.start(this._cartoDBLayerGroupView); + this._mapCursorManager.start(this._cartoDBLayerGroupView); + this._mapEventsManager.start(this._cartoDBLayerGroupView); + + return this._cartoDBLayerGroupView; + }, + + _addIndividualLayer: function (layerModel) { + var layerView = this._createLayerView(layerModel); + if (layerView) { + this._layerViews[layerModel.cid] = layerView; + } + return layerView; + }, + + _createLayerView: function (layerModel) { + return this._getLayerViewFactory().createLayerView(layerModel, { + nativeMap: this.getNativeMap(), + mapModel: this.map, + showLimitErrors: this._showLimitErrors + }); + }, + + _removeLayers: function () { + var layerViews = _.uniq(_.values(this._layerViews)); + for (var i in layerViews) { + var layerView = layerViews[i]; + layerView.remove(); + } + this._layerViews = {}; + }, + + _removeLayer: function (layerModel) { + var layerView = this._layerViews[layerModel.cid]; + if (layerView) { + if (layerModel.get('type') === 'CartoDB') { + if (this.map.layers.getCartoDBLayers().length === 0) { + layerView.remove(); + this._cartoDBLayerGroupView = null; + } + } else { + layerView.remove(); + } + delete this._layerViews[layerModel.cid]; + } + }, + + getLayerViewByLayerCid: function (cid) { + var l = this._layerViews[cid]; + if (!l) { + log.debug('layer with cid ' + cid + " can't be get"); + } + return l; + }, + + setCursor: function () { + throw new Error('subclasses of MapView must implement setCursor'); + }, + + getSize: function () { + throw new Error('subclasses of MapView must implement getSize'); + }, + + addMarker: function (marker) { + throw new Error('subclasses of MapView must implement addMarker'); + }, + + removeMarker: function (marker) { + throw new Error('subclasses of MapView must implement removeMarker'); + }, + + hasMarker: function (marker) { + throw new Error('subclasses of MapView must implement hasMarker'); + }, + + addPath: function (path) { + throw new Error('subclasses of MapView must implement addPath'); + }, + + removePath: function (path) { + throw new Error('subclasses of MapView must implement removePath'); + }, + + // returns { x: 100, y: 200 } + latLngToContainerPoint: function (latlng) { + throw new Error('subclasses of MapView must implement latLngToContainerPoint'); + }, + + // returns { lat: 0, lng: 0} + containerPointToLatLng: function (point) { + throw new Error('subclasses of MapView must implement containerPointToLatLng'); + }, + + getNativeMap: function () { + throw new Error('Subclasses of src/geo/map-view.js must implement getNativeMap'); + }, + + _getLayerViewFactory: function () { + throw new Error('subclasses of MapView must implement _getLayerViewFactory'); + }, + + _addLayerToMap: function () { + throw new Error('Subclasses of src/geo/map-view.js must implement _addLayerToMap'); + }, + + _setZoom: function (model, z) { + throw new Error('Subclasses of src/geo/map-view.js must implement _setZoom'); + }, + + _setCenter: function (model, center) { + throw new Error('Subclasses of src/geo/map-view.js must implement _setCenter'); + }, + + _addGeomToMap: function (geom) { + throw new Error('Subclasses of src/geo/map-view.js must implement _addGeomToMap'); + }, + + _removeGeomFromMap: function (geo) { + throw new Error('Subclasses of src/geo/map-view.js must implement _removeGeomFromMap'); + }, + + _createNativeMap: function () { + throw new Error('Subclasses of src/geo/map-view.js must implement _createNativeMap'); + } +}); + +module.exports = MapView; diff --git a/src/geo/map.js b/src/geo/map.js new file mode 100644 index 0000000..1317fe4 --- /dev/null +++ b/src/geo/map.js @@ -0,0 +1,515 @@ +/* global L */ +var _ = require('underscore'); +var Backbone = require('backbone'); +var config = require('cdb.config'); +var log = require('cdb.log'); +var Model = require('../core/model'); +var util = require('../core/util'); +var Layers = require('./map/layers'); +var sanitize = require('../core/sanitize'); +var GeometryFactory = require('./geometry-models/geometry-factory'); + +var Map = Model.extend({ + defaults: { + attribution: [config.get('cartodb_attributions')], + center: [0, 0], + zoom: 4, + minZoom: 0, + maxZoom: 20, + scrollwheel: true, + drag: true, + keyboard: !util.supportsTouch(), // #cartodb.js/1652 + provider: 'leaflet', + popupsEnabled: true, + isFeatureInteractivityEnabled: false + }, + + initialize: function (attrs, options) { + options = options || {}; + + if (!options.layersFactory) throw new Error('layersFactory is required'); + this._layersFactory = options.layersFactory; + + attrs = attrs || {}; + + this.layers = options.layersCollection || new Layers(); + this.geometries = new Backbone.Collection(); + + var center = attrs.center || this.defaults.center; + if (typeof center === 'string') { + center = JSON.parse(center); + } + + this.set({ + center: center, + original_center: center + }); + + if (attrs.bounds) { + this.set({ + view_bounds_sw: attrs.bounds[0], + view_bounds_ne: attrs.bounds[1] + }); + this.unset('bounds'); + } else { + this.set({ + zoom: attrs.zoom || this.defaults.zoom + }); + } + + this.layers.bind('reset', this._updateAttributions, this); + this.layers.bind('add', this._updateAttributions, this); + this.layers.bind('remove', this._updateAttributions, this); + this.layers.bind('change:attribution', this._updateAttributions, this); + this.layers.bind('reset', this._onLayersResetted, this); + + this._updateAttributions(); + }, + + _onLayersResetted: function () { + if (this.layers.size() >= 1) { + this._adjustZoomtoLayer(this.layers.first()); + } + }, + + _updateAttributions: function () { + var defaultCartoDBAttribution = this.defaults.attribution[0]; + var attributions = _.chain(this.layers.models) + .map(function (layer) { return sanitize.html(layer.get('attribution')); }) + .reject(function (attribution) { return attribution === defaultCartoDBAttribution; }) + .compact() + .uniq() + .value(); + + attributions.push(defaultCartoDBAttribution); + + this.set('attribution', attributions); + }, + + // PUBLIC API METHODS + moveCartoDBLayer: function (from, to) { + var layerMoved = this.layers.moveCartoDBLayer(from, to); + if (layerMoved) { + this.trigger('cartodbLayerMoved', {}, this); + } + }, + + createCartoDBLayer: function (attrs, options) { + return this._addNewLayerModel('cartodb', attrs, options); + }, + + createTorqueLayer: function (attrs, options) { + return this._addNewLayerModel('torque', attrs, options); + }, + + createTileLayer: function (attrs, options) { + return this._addNewLayerModel('tiled', attrs, options); + }, + + createWMSLayer: function (attrs, options) { + return this._addNewLayerModel('wms', attrs, options); + }, + + createGMapsBaseLayer: function (attrs, options) { + return this._addNewLayerModel('gmapsbase', attrs, options); + }, + + createPlainLayer: function (attrs, options) { + return this._addNewLayerModel('plain', attrs, options); + }, + + _addNewLayerModel: function (type, attrs, options) { + options = options || {}; + var layerModel = this._layersFactory.createLayer(type, attrs); + this.listenTo(layerModel, 'destroy', this._removeLayerModelFromCollection); + this.layers.add(layerModel, { + silent: options.silent, + at: options.at + }); + + return layerModel; + }, + + _removeLayerModelFromCollection: function (layerModel, collection, opts) { + return this.layers.remove(layerModel, opts); + }, + + disableInteractivity: function () { + this.disablePopups(); + this.disableFeatureInteractivity(); + }, + + enableInteractivity: function () { + this.enablePopups(); + this.enableFeatureInteractivity(); + }, + + enableFeatureInteractivity: function () { + this.set('isFeatureInteractivityEnabled', true); + }, + + disableFeatureInteractivity: function () { + this.set('isFeatureInteractivityEnabled', false); + }, + + isFeatureInteractivityEnabled: function () { + return !!this.get('isFeatureInteractivityEnabled'); + }, + + enablePopups: function () { + this.set('popupsEnabled', true); + }, + + disablePopups: function () { + this.set('popupsEnabled', false); + }, + + arePopupsEnabled: function () { + return !!this.get('popupsEnabled'); + }, + + arePopupsDisabled: function () { + return !this.arePopupsEnabled(); + }, + + // GEOMETRY MANAGEMENT + + drawPoint: function () { + return this._drawGeometry(GeometryFactory.createPoint({ + editable: true + })); + }, + + drawPolyline: function () { + return this._drawGeometry(GeometryFactory.createPolyline({ + editable: true + })); + }, + + drawPolygon: function () { + return this._drawGeometry(GeometryFactory.createPolygon({ + editable: true + })); + }, + + _drawGeometry: function (geometry) { + this.trigger('enterDrawingMode', geometry); + return geometry; + }, + + stopDrawingGeometry: function () { + this.trigger('exitDrawingMode'); + }, + + editGeometry: function (geoJSON) { + var geometry = GeometryFactory.createGeometryFromGeoJSON(geoJSON, { + editable: true, + expandable: true + }); + this.trigger('enterEditMode', geometry); + return geometry; + }, + + stopEditingGeometry: function (geoJSON) { + this.trigger('exitEditMode'); + }, + + // INTERNAL CartoDB.js METHODS + + setView: function (latlng, zoom, options) { + var setViewOptions = options || { silent: true }; + + this.set({ + center: latlng, + zoom: zoom + }, setViewOptions); + + this.trigger('set_view'); + }, + + setZoom: function (z) { + this.set({ + zoom: z + }); + }, + + enableKeyboard: function () { + this.set({ + keyboard: true + }); + }, + + disableKeyboard: function () { + this.set({ + keyboard: false + }); + }, + + enableScrollWheel: function () { + this.set({ + scrollwheel: true + }); + }, + + disableScrollWheel: function () { + this.set({ + scrollwheel: false + }); + }, + + getZoom: function () { + return this.get('zoom'); + }, + + setCenter: function (latlng) { + this.set({ + center: latlng + }); + }, + + /** + * Change multiple options at the same time + * @params {Object} New options object + */ + setOptions: function (options) { + if (typeof options !== 'object' || options.length) { + if (this.options.debug) { + throw Error(options + ' options has to be an object'); + } else { + return; + } + } + + // Set options + _.defaults(this.options, options); + }, + + /** + * return getViewbounds if it is set + */ + getViewBounds: function () { + if (this.has('view_bounds_sw') && this.has('view_bounds_ne')) { + return [ + this.get('view_bounds_sw'), + this.get('view_bounds_ne') + ]; + } + return [[0, 0], [0, 0]]; + }, + + getLayerAt: function (i) { + return this.layers.at(i); + }, + + getLayerById: function (id) { + return this.layers.get(id); + }, + + getLayerViewByLayerCid: function (cid) { + return this.layers.get(cid); + }, + + _adjustZoomtoLayer: function (layer) { + var maxZoom = parseInt(layer.get('maxZoom'), 10); + var minZoom = parseInt(layer.get('minZoom'), 10); + + if (_.isNumber(maxZoom) && !_.isNaN(maxZoom)) { + if (this.get('zoom') > maxZoom) this.set({ zoom: maxZoom, maxZoom: maxZoom }); + else this.set('maxZoom', maxZoom); + } + + if (_.isNumber(minZoom) && !_.isNaN(minZoom)) { + if (this.get('zoom') < minZoom) this.set({ minZoom: minZoom, zoom: minZoom }); + else this.set('minZoom', minZoom); + } + }, + + addLayer: function (layer, opts) { + if (this.layers.size() === 0) { + this._adjustZoomtoLayer(layer); + } + this.layers.add(layer, opts); + this.trigger('layerAdded'); + if (this.layers.length === 1) { + this.trigger('firstLayerAdded'); + } + return layer.cid; + }, + + removeLayer: function (layer) { + this.layers.remove(layer); + }, + + removeLayerByCid: function (cid) { + var layer = this.layers.get(cid); + + if (layer) this.removeLayer(layer); + else log.error("There's no layer with cid = " + cid + '.'); + }, + + removeLayerAt: function (i) { + var layer = this.layers.at(i); + + if (layer) this.removeLayer(layer); + else log.error("There's no layer in that position."); + }, + + clearLayers: function () { + while (this.layers.length > 0) { + this.removeLayer(this.layers.at(0)); + } + }, + + // by default the base layer is the layer at index 0 + getBaseLayer: function () { + return this.layers.at(0); + }, + + /** + * gets the url of the template of the tile layer + * @method getLayerTemplate + */ + getLayerTemplate: function () { + var baseLayer = this.getBaseLayer(); + if (baseLayer && baseLayer.get('options')) { + return baseLayer.get('options').urlTemplate; + } + }, + + addGeometry: function (geom) { + this.geometries.add(geom); + }, + + removeGeometry: function (geom) { + this.geometries.remove(geom); + }, + + setBounds: function (b) { + this.attributes.view_bounds_sw = [ + b[0][0], + b[0][1] + ]; + this.attributes.view_bounds_ne = [ + b[1][0], + b[1][1] + ]; + + // change both at the same time + this.trigger('change:view_bounds_ne', this); + }, + + // set center and zoom according to fit bounds + fitBounds: function (bounds, mapSize) { + var z = this.getBoundsZoom(bounds, mapSize); + if (z === null) { + return; + } + + // project -> calculate center -> unproject + var swPoint = Map.latlngToMercator(bounds[0], z); + var nePoint = Map.latlngToMercator(bounds[1], z); + + var center = Map.mercatorToLatLng({ + x: (swPoint[0] + nePoint[0]) * 0.5, + y: (swPoint[1] + nePoint[1]) * 0.5 + }, z); + this.set({ + center: center, + zoom: z + }); + }, + + // adapted from leaflat src + // @return {Number, null} Calculated zoom from given bounds or the maxZoom if no appropriate zoom level could be found + // or null if given mapSize has no size. + getBoundsZoom: function (boundsSWNE, mapSize) { + // sometimes the map reports size = 0 so return null + if (mapSize.x === 0 || mapSize.y === 0) { + return null; + } + var size = [mapSize.x, mapSize.y]; + var zoom = this.get('minZoom') || 0; + var maxZoom = this.get('maxZoom') || 24; + var ne = boundsSWNE[1]; + var sw = boundsSWNE[0]; + var boundsSize = []; + var nePoint; + var swPoint; + var zoomNotFound = true; + + do { + zoom++; + nePoint = Map.latlngToMercator(ne, zoom); + swPoint = Map.latlngToMercator(sw, zoom); + boundsSize[0] = Math.abs(nePoint[0] - swPoint[0]); + boundsSize[1] = Math.abs(swPoint[1] - nePoint[1]); + zoomNotFound = boundsSize[0] <= size[0] && boundsSize[1] <= size[1]; + } while (zoomNotFound && zoom <= maxZoom); + + if (zoomNotFound) { + return maxZoom; + } + + return zoom - 1; + }, + + setPixelToLatLngConverter: function (pixelToLatLngConverter) { + this._pixelToLatLngConverter = pixelToLatLngConverter; + }, + + setLatLngToPixelConverter: function (latLngToPixelConverter) { + this._latLngToPixelConverter = latLngToPixelConverter; + }, + + pixelToLatLng: function () { + return this._pixelToLatLngConverter; + }, + + latLngToPixel: function () { + return this._latLngToPixelConverter; + }, + + setMapViewSize: function (size) { + this._mapViewSize = size; + this.trigger('mapViewSizeChanged'); + }, + + getMapViewSize: function () { + return this._mapViewSize; + }, + + getEstimatedFeatureCount: function () { + var acum = 0; + var count = 0; + var layers = this.layers.getCartoDBLayers(); + if (!layers) { + return; + } + for (var i = 0; i < layers.length; i++) { + count = layers[i].getEstimatedFeatureCount(); + if (count === undefined) { + return; + } + acum += count; + } + return acum; + } +}, { + PROVIDERS: { + GMAPS: 'googlemaps', + LEAFLET: 'leaflet' + }, + + latlngToMercator: function (latlng, zoom) { + var ll = new L.LatLng(latlng[0], latlng[1]); + var pp = L.CRS.EPSG3857.latLngToPoint(ll, zoom); + return [pp.x, pp.y]; + }, + + mercatorToLatLng: function (point, zoom) { + var ll = L.CRS.EPSG3857.pointToLatLng(point, zoom); + return [ll.lat, ll.lng]; + } +}); + +module.exports = Map; diff --git a/src/geo/map/cartodb-layer.js b/src/geo/map/cartodb-layer.js new file mode 100644 index 0000000..b39c4d7 --- /dev/null +++ b/src/geo/map/cartodb-layer.js @@ -0,0 +1,170 @@ +var _ = require('underscore'); +var config = require('../../cdb.config'); +var LayerModelBase = require('./layer-model-base'); +var InfowindowTemplate = require('./infowindow-template'); +var TooltipTemplate = require('./tooltip-template'); +var Legends = require('./legends/legends'); +var AnalysisModel = require('../../analysis/analysis-model'); + +var ATTRIBUTES_THAT_TRIGGER_VIS_RELOAD = ['sql', 'source', 'sql_wrap', 'cartocss']; + +var CartoDBLayer = LayerModelBase.extend({ + defaults: { + type: 'CartoDB', + attribution: config.get('cartodb_attributions'), + visible: true + }, + + initialize: function (attrs, options) { + attrs = attrs || {}; + options = options || {}; + if (!options.engine) throw new Error('engine is required'); + + this._engine = options.engine; + if (attrs && attrs.cartocss) { + this.set('initialStyle', attrs.cartocss); + } + + if (attrs.source) { + this.setSource(attrs.source); + } + + // PUBLIC PROPERTIES + this.infowindow = new InfowindowTemplate(attrs.infowindow); + this.tooltip = new TooltipTemplate(attrs.tooltip); + this.unset('infowindow'); + this.unset('tooltip'); + + this.legends = new Legends(attrs.legends, { engine: this._engine }); + this.unset('legends'); + + this.bind('change', this._onAttributeChanged, this); + this.infowindow.fields.bind('reset add remove', this._reload, this); + this.tooltip.fields.bind('reset add remove', this._reload, this); + + this.aggregation = options.aggregation; + + LayerModelBase.prototype.initialize.apply(this, arguments); + }, + + _onAttributeChanged: function () { + var reload = _.any(ATTRIBUTES_THAT_TRIGGER_VIS_RELOAD, function (attr) { + if (this.hasChanged(attr)) { + return true; + } + }, this); + + if (reload) { + this._reload(); + } + }, + + _reload: function () { + this._engine.reload({ + sourceId: this.get('id') + }); + }, + + restoreCartoCSS: function () { + this.set('cartocss', this.get('initialStyle')); + }, + + isVisible: function () { + return this.get('visible'); + }, + + isInteractive: function () { + return this._hasInfowindowFields() || this._hasTooltipFields(); + }, + + _hasInfowindowFields: function () { + return this.infowindow.hasFields(); + }, + + _hasTooltipFields: function () { + return this.tooltip.hasFields(); + }, + + getInteractiveColumnNames: function () { + return _.chain(['cartodb_id']) + .union(this.infowindow.getFieldNames()) + .union(this.tooltip.getFieldNames()) + .uniq() + .value(); + }, + + isInfowindowEnabled: function () { + return this.infowindow.hasTemplate(); + }, + + isTooltipEnabled: function () { + return this.tooltip.hasTemplate(); + }, + + getName: function () { + return this.get('layer_name'); + }, + + getEstimatedFeatureCount: function () { + var meta = this.get('meta'); + var stats = meta && meta.stats; + return stats && stats.estimatedFeatureCount; + }, + + getSourceId: function () { + var source = this.getSource(); + return source && source.id; + }, + + getSource: function () { + return this.get('source'); + }, + + setSource: function (newSource, options) { + if (this.getSource()) { + this.getSource().unmarkAsSourceOf(this); + } + newSource.markAsSourceOf(this); + this.set('source', newSource, options); + }, + + /** + * Check if an analysis node is the layer's source. + * Only torque and cartodb layers have a source otherwise return false. + */ + hasSource: function (analysisModel) { + return this.getSource().equals(analysisModel); + }, + + update: function (attrs) { + if (attrs.source) { + throw new Error('Use ".setSource" to update a layer\'s source instead of the update method'); + } + LayerModelBase.prototype.update.apply(this, arguments); + }, + + remove: function () { + this.getSource().unmarkAsSourceOf(this); + LayerModelBase.prototype.remove.apply(this, arguments); + }, + + getTableName: function () { + if (this.get('source').has('options')) { + return this.get('source').get('options').table_name; + } + }, + + getApiKey: function () { + return this._engine.getApiKey(); + } +}, + // Static methods and properties +{ + _checkSourceAttribute: function (source) { + if (!(source instanceof AnalysisModel)) { + throw new Error('Source must be an instance of AnalysisModel'); + } + } +}); + +module.exports = CartoDBLayer; diff --git a/src/geo/map/gmaps-base-layer.js b/src/geo/map/gmaps-base-layer.js new file mode 100644 index 0000000..b51a560 --- /dev/null +++ b/src/geo/map/gmaps-base-layer.js @@ -0,0 +1,13 @@ +var LayerModelBase = require('./layer-model-base'); + +var GMapsBaseLayer = LayerModelBase.extend({ + OPTIONS: ['roadmap', 'satellite', 'terrain', 'custom'], + defaults: { + type: 'GMapsBase', + visible: true, + baseType: 'gray_roadmap', + style: null + } +}); + +module.exports = GMapsBaseLayer; diff --git a/src/geo/map/infowindow-template.js b/src/geo/map/infowindow-template.js new file mode 100644 index 0000000..3557845 --- /dev/null +++ b/src/geo/map/infowindow-template.js @@ -0,0 +1,47 @@ +var _ = require('underscore'); +var Backbone = require('backbone'); +var PopupFields = require('./popup-fields'); + +var InfowindowTemplate = Backbone.Model.extend({ + defaults: { + offset: [28, 0], // offset of the tip calculated from the bottom left corner + maxHeight: 180, // max height of the content, not the whole infowindow + alternative_names: { }, + template_type: 'mustache' + }, + + initialize: function (attrs) { + attrs = attrs || {}; + this.fields = new PopupFields(attrs.fields || []); + this.unset('fields'); + }, + + update: function (attrs) { + attrs = _.clone(attrs); + + if (!this.fields.equals(attrs.fields)) { + this.fields.reset(attrs.fields); + } + delete attrs.fields; + + if (attrs.alternative_names) { + attrs.alternative_names = JSON.parse(JSON.stringify(attrs.alternative_names)); + } + + this.set(attrs); + }, + + getFieldNames: function () { + return this.fields.pluck('name'); + }, + + hasFields: function () { + return !this.fields.isEmpty(); + }, + + hasTemplate: function () { + return !!this.get('template'); + } +}); + +module.exports = InfowindowTemplate; diff --git a/src/geo/map/layer-model-base.js b/src/geo/map/layer-model-base.js new file mode 100644 index 0000000..d916f7f --- /dev/null +++ b/src/geo/map/layer-model-base.js @@ -0,0 +1,77 @@ +var log = require('../../cdb.log'); +var Model = require('../../core/model'); + +// Map layer, could be tiled or whatever +var MapLayer = Model.extend({ + + initialize: function () { + this.bind('change:type', function () { + log.error('changing layer type is not allowed, remove it and add a new one instead'); + }); + }, + + // PUBLIC API METHODS + + remove: function (opts) { + opts = opts || {}; + this.trigger('destroy', this, this.collection, opts); + }, + + update: function (attrs, options) { + options = options || {}; + + // TODO: Pick the attributes for the specific type of layer + // Eg: this.set(_.pick(attrs, this.ATTR_NAMES)) + this.set(attrs, { + silent: options.silent + }); + }, + + show: function () { + this.set('visible', true); + }, + + hide: function () { + this.set('visible', false); + }, + + isVisible: function () { + return !!this.get('visible'); + }, + + isHidden: function () { + return !this.isVisible(); + }, + + toggle: function () { + this.set('visible', !this.get('visible')); + }, + + // INTERNAL CartoDB.js METHODS + + setOk: function () { + this.unset('error'); + }, + + setError: function (error) { + this.set('error', error); + }, + + /** + * Only torque and cartodb layers have a source. + * @abstract + */ + getSourceId: function () { + throw new Error('.getSourceId called on a non torque/cartodb layer.'); + }, + + /** + * Check if an analysis node is the layer's source. + * Only torque and cartodb layers have a source otherwise return false. + */ + hasSource: function (analysisNode) { + return false; + } +}); + +module.exports = MapLayer; diff --git a/src/geo/map/layer-types.js b/src/geo/map/layer-types.js new file mode 100644 index 0000000..39cc17b --- /dev/null +++ b/src/geo/map/layer-types.js @@ -0,0 +1,36 @@ +var TILED_LAYER_TYPE = 'Tiled'; +var PLAIN_LAYER_TYPE = 'Plain'; +var WMS_LAYER_TYPE = 'WMS'; +var GMAPS_BASE_LAYER_TYPE = 'GMapsBase'; +var CARTODB_LAYER_TYPE = 'CartoDB'; +var TORQUE_LAYER_TYPE = 'torque'; + +var isLayerOfType = function (layerModel, layerType) { + return layerModel.get('type').toLowerCase() === layerType.toLowerCase(); +}; + +module.exports = { + isTiledLayer: function (layerModel) { + return isLayerOfType(layerModel, TILED_LAYER_TYPE); + }, + + isPlainLayer: function (layerModel) { + return isLayerOfType(layerModel, PLAIN_LAYER_TYPE); + }, + + isWMSLayer: function (layerModel) { + return isLayerOfType(layerModel, WMS_LAYER_TYPE); + }, + + isGoogleMapsBaseLayer: function (layerModel) { + return isLayerOfType(layerModel, GMAPS_BASE_LAYER_TYPE); + }, + + isCartoDBLayer: function (layerModel) { + return isLayerOfType(layerModel, CARTODB_LAYER_TYPE); + }, + + isTorqueLayer: function (layerModel) { + return isLayerOfType(layerModel, TORQUE_LAYER_TYPE); + } +}; diff --git a/src/geo/map/layers.js b/src/geo/map/layers.js new file mode 100644 index 0000000..c25acaf --- /dev/null +++ b/src/geo/map/layers.js @@ -0,0 +1,100 @@ +var _ = require('underscore'); +var Backbone = require('backbone'); + +var TILED_LAYER_TYPE = 'Tiled'; +var CARTODB_LAYER_TYPE = 'CartoDB'; +var TORQUE_LAYER_TYPE = 'torque'; + +var Layers = Backbone.Collection.extend({ + + initialize: function () { + this.comparator = function (m) { + return parseInt(m.get('order'), 10); + }; + this.bind('add', this._assignIndexes); + this.bind('remove', this._assignIndexes); + }, + + /** + * each time a layer is added or removed + * the index should be recalculated + */ + _assignIndexes: function (model, col, options) { + if (this.size() > 0) { + // Assign an order of 0 to the first layer + this.at(0).set({ order: 0 }); + + if (this.size() > 1) { + var layersByType = this.reduce(function (layersByType, layerModel, index) { + var type = layerModel.get('type'); + if (index === 0 && type === TILED_LAYER_TYPE) { return layersByType; } + layersByType[type] = layersByType[type] || []; + layersByType[type].push(layerModel); + return layersByType; + }, {}); + + var lastOrder = 1; + var sortedTypes = [CARTODB_LAYER_TYPE, TORQUE_LAYER_TYPE, TILED_LAYER_TYPE]; + _.each(sortedTypes, function (layerType) { + var layers = layersByType[layerType] || []; + _.each(layers, function (layerModel) { + layerModel.set({ + order: lastOrder + }); + lastOrder += 1; + }); + }); + } + } + + this.sort(); + }, + + getCartoDBLayers: function () { + return this._getLayersByType(CARTODB_LAYER_TYPE); + }, + + getTiledLayers: function () { + return this._getLayersByType(TILED_LAYER_TYPE); + }, + + getTorqueLayers: function () { + return this._getLayersByType(TORQUE_LAYER_TYPE); + }, + + _getLayersByType: function (layerType) { + return this.select(function (layerModel) { + return layerModel.get('type') === layerType; + }); + }, + + getLayersWithLegends: function () { + return this.select(function (layerModel) { + return !!layerModel.legends; + }); + }, + + moveCartoDBLayer: function (from, to) { + if (from === to) { + return false; + } + + var movingLayer = this.at(from); + + if (!movingLayer || movingLayer.get('type') !== CARTODB_LAYER_TYPE) { + return false; + } + + this.remove(movingLayer, { silent: true }); + this.add(movingLayer, { + at: to, + silent: true + }); + + this.trigger('layerMoved', movingLayer); + + return movingLayer; + } +}); + +module.exports = Layers; diff --git a/src/geo/map/legends/bubble-legend-model.js b/src/geo/map/legends/bubble-legend-model.js new file mode 100644 index 0000000..0f9f525 --- /dev/null +++ b/src/geo/map/legends/bubble-legend-model.js @@ -0,0 +1,27 @@ +var _ = require('underscore'); +var LegendModelBase = require('./legend-model-base'); + +var BubbleLegendModel = LegendModelBase.extend({ + defaults: function () { + return _.extend(LegendModelBase.prototype.defaults.apply(this), { + type: 'bubble', + fillColor: '', + prefix: '', + suffix: '', + values: [] + }); + }, + + getNonResettableAttrs: function () { + return _.union( + LegendModelBase.prototype.getNonResettableAttrs.apply(this), + ['values'] + ); + }, + + isAvailable: function () { + return this.get('values') && this.get('values').length > 0; + } +}); + +module.exports = BubbleLegendModel; diff --git a/src/geo/map/legends/category-legend-model.js b/src/geo/map/legends/category-legend-model.js new file mode 100644 index 0000000..7462c64 --- /dev/null +++ b/src/geo/map/legends/category-legend-model.js @@ -0,0 +1,24 @@ +var _ = require('underscore'); +var LegendModelBase = require('./legend-model-base'); + +var CategoryLegendModel = LegendModelBase.extend({ + defaults: function () { + return _.extend(LegendModelBase.prototype.defaults.apply(this), { + type: 'category', + categories: [] + }); + }, + + getNonResettableAttrs: function () { + return _.union( + LegendModelBase.prototype.getNonResettableAttrs.apply(this), + ['categories'] + ); + }, + + isAvailable: function () { + return this.get('categories') && this.get('categories').length > 0; + } +}); + +module.exports = CategoryLegendModel; diff --git a/src/geo/map/legends/choropleth-legend-model.js b/src/geo/map/legends/choropleth-legend-model.js new file mode 100644 index 0000000..583dceb --- /dev/null +++ b/src/geo/map/legends/choropleth-legend-model.js @@ -0,0 +1,28 @@ +var _ = require('underscore'); +var LegendModelBase = require('./legend-model-base'); + +var ChoroplethLegendModel = LegendModelBase.extend({ + defaults: function () { + return _.extend(LegendModelBase.prototype.defaults.apply(this), { + type: 'choropleth', + prefix: '', + suffix: '', + leftLabel: '', + rightLabel: '', + colors: [] + }); + }, + + getNonResettableAttrs: function () { + return _.union( + LegendModelBase.prototype.getNonResettableAttrs.apply(this), + ['colors'] + ); + }, + + isAvailable: function () { + return this.get('colors') && this.get('colors').length > 0; + } +}); + +module.exports = ChoroplethLegendModel; diff --git a/src/geo/map/legends/custom-choropleth-legend-model.js b/src/geo/map/legends/custom-choropleth-legend-model.js new file mode 100644 index 0000000..6e41221 --- /dev/null +++ b/src/geo/map/legends/custom-choropleth-legend-model.js @@ -0,0 +1,17 @@ +var _ = require('underscore'); +var StaticLegendModelBase = require('./static-legend-model-base'); + +var CustomChoroplethLegendModel = StaticLegendModelBase.extend({ + defaults: function () { + return _.extend(StaticLegendModelBase.prototype.defaults.apply(this), { + type: 'custom_choropleth', + prefix: '', + suffix: '', + leftLabel: '', + rightLabel: '', + colors: [] + }); + } +}); + +module.exports = CustomChoroplethLegendModel; diff --git a/src/geo/map/legends/custom-legend-model.js b/src/geo/map/legends/custom-legend-model.js new file mode 100644 index 0000000..16d2183 --- /dev/null +++ b/src/geo/map/legends/custom-legend-model.js @@ -0,0 +1,14 @@ +var _ = require('underscore'); +var StaticLegendModelBase = require('./static-legend-model-base'); + +var CustomLegendModel = StaticLegendModelBase.extend({ + defaults: function () { + return _.extend(StaticLegendModelBase.prototype.defaults.apply(this), { + type: 'custom', + items: [], + html: '' + }); + } +}); + +module.exports = CustomLegendModel; diff --git a/src/geo/map/legends/legend-model-base.js b/src/geo/map/legends/legend-model-base.js new file mode 100644 index 0000000..ea03346 --- /dev/null +++ b/src/geo/map/legends/legend-model-base.js @@ -0,0 +1,76 @@ +var _ = require('underscore'); +var Backbone = require('backbone'); +var Engine = require('../../../engine'); + +var NON_RESETEABLE_DEFAULT_ATTRS = ['state', 'visible']; + +var LegendModelBase = Backbone.Model.extend({ + defaults: function () { + return { + visible: false, + title: '', + preHTMLSnippet: '', + postHTMLSnippet: '', + state: this.constructor.STATE_LOADING + }; + }, + + initialize: function (attrs, deps) { + if (!deps.engine) throw new Error('engine is required'); + + deps.engine.on(Engine.Events.RELOAD_STARTED, this._onEngineReloadStarted, this); + }, + + _onEngineReloadStarted: function () { + this.set('state', this.constructor.STATE_LOADING); + }, + + isLoading: function () { + return this.get('state') === this.constructor.STATE_LOADING; + }, + + isError: function () { + return this.get('state') === this.constructor.STATE_ERROR; + }, + + isSuccess: function () { + return this.get('state') === this.constructor.STATE_SUCCESS; + }, + + show: function () { + this.set('visible', true); + }, + + hide: function () { + this.set('visible', false); + }, + + isVisible: function () { + return this.get('visible'); + }, + + update: function (attrs) { + this.set(attrs); + }, + + getNonResettableAttrs: function () { + return NON_RESETEABLE_DEFAULT_ATTRS; + }, + + reset: function () { + var defaults = _.omit(this.defaults(), + this.getNonResettableAttrs() + ); + this.set(defaults); + }, + + isAvailable: function () { + return false; + } +}, { + STATE_LOADING: 'loading', + STATE_SUCCESS: 'success', + STATE_ERROR: 'error' +}); + +module.exports = LegendModelBase; diff --git a/src/geo/map/legends/legends.js b/src/geo/map/legends/legends.js new file mode 100644 index 0000000..38971f0 --- /dev/null +++ b/src/geo/map/legends/legends.js @@ -0,0 +1,112 @@ +var _ = require('underscore'); +var CategoryLegendModel = require('./category-legend-model'); +var BubbleLegendModel = require('./bubble-legend-model'); +var ChoroplethLegendModel = require('./choropleth-legend-model'); +var CustomLegendModel = require('./custom-legend-model'); +var CustomChoroplethLegendModel = require('./custom-choropleth-legend-model'); +var TorqueLegendModel = require('./torque-legend-model'); + +var LEGENDS_METADATA = { + bubble: { + modelClass: BubbleLegendModel, + definitionAttrs: [ { 'fillColor': 'color' }, {'topLabel': 'top_label'}, {'bottomLabel': 'bottom_label'} ], + dynamic: true + }, + category: { + modelClass: CategoryLegendModel, + definitionAttrs: [ 'prefix', 'suffix' ], + dynamic: true + }, + choropleth: { + modelClass: ChoroplethLegendModel, + definitionAttrs: [ 'prefix', 'suffix', {'leftLabel': 'left_label'}, {'rightLabel': 'right_label'} ], + dynamic: true + }, + custom: { + modelClass: CustomLegendModel, + definitionAttrs: [ { 'items': 'categories' }, 'html' ] + }, + html: { + modelClass: CustomLegendModel, + definitionAttrs: [ { 'items': 'categories' }, 'html' ] + }, + custom_choropleth: { + modelClass: CustomChoroplethLegendModel, + definitionAttrs: [ 'prefix', 'suffix', {'leftLabel': 'left_label'}, {'rightLabel': 'right_label'}, 'colors' ] + }, + torque: { + modelClass: TorqueLegendModel, + definitionAttrs: [ { 'items': 'categories' }, 'html' ] + } +}; + +var SHARED_ATTRS = [ + 'title', + { 'preHTMLSnippet': 'pre_html' }, + { 'postHTMLSnippet': 'post_html' } +]; + +var Legends = function (legendsData, deps) { + if (!deps.engine) throw new Error('engine is required'); + + this._legendsData = legendsData || []; + this._engine = deps.engine; + + _.each(LEGENDS_METADATA, function (legendMetadata, legendType) { + this[legendType] = this._createLegendModel(legendType, legendMetadata); + }, this); + + // HOTFIX: TO BE DELETED ONCE MIGRATED + var data = this._findDataForLegend('html'); + if (data) { + this['custom'] = this['html']; + } + + delete this['html']; +}; + +Legends.prototype._createLegendModel = function (legendType, legendMetadata) { + var ModelClass = legendMetadata.modelClass; + var attrs = SHARED_ATTRS.concat(legendMetadata.definitionAttrs); + var data = this._findDataForLegend(legendType); + + // Flatten data.definition + data = data && _.extend({}, + _.omit(data, 'definition'), + data.definition); + + var modelAttrs = {}; + _.each(attrs, function (attr) { + var attrNameInData = attr; + var attrNameForModel = attr; + if (_.isObject(attr)) { + attrNameForModel = Object.keys(attr)[0]; + attrNameInData = attr[attrNameForModel]; + } + + modelAttrs[attrNameForModel] = data && data[attrNameInData]; + }); + + var legendModel = new ModelClass(modelAttrs, { + engine: this._engine + }); + + if (data) { + legendModel.show(); + } + return legendModel; +}; + +Legends.prototype._findDataForLegend = function (legendType) { + return _.find(this._legendsData, { type: legendType }); +}; + +Legends.prototype.hasAnyLegend = function () { + var legendTypes = _.keys(LEGENDS_METADATA); + return _.some(legendTypes, function (legendType) { + var legend = this[legendType]; + return legend && legend.isVisible(); + }, this); +}; + +module.exports = Legends; diff --git a/src/geo/map/legends/static-legend-model-base.js b/src/geo/map/legends/static-legend-model-base.js new file mode 100644 index 0000000..e462cfd --- /dev/null +++ b/src/geo/map/legends/static-legend-model-base.js @@ -0,0 +1,16 @@ +var _ = require('underscore'); +var LegendModelBase = require('./legend-model-base'); + +var StaticLegendModelBase = LegendModelBase.extend({ + defaults: function () { + return _.extend(LegendModelBase.prototype.defaults.apply(this), { + state: LegendModelBase.STATE_SUCCESS + }); + }, + + _onEngineReloadStarted: function () {}, + + isAvailable: function () { return true; } +}); + +module.exports = StaticLegendModelBase; diff --git a/src/geo/map/legends/torque-legend-model.js b/src/geo/map/legends/torque-legend-model.js new file mode 100644 index 0000000..7dcf824 --- /dev/null +++ b/src/geo/map/legends/torque-legend-model.js @@ -0,0 +1,14 @@ +var _ = require('underscore'); +var StaticLegendModelBase = require('./static-legend-model-base'); + +var TorqueLegendModel = StaticLegendModelBase.extend({ + defaults: function () { + return _.extend(StaticLegendModelBase.prototype.defaults.apply(this), { + type: 'torque', + items: [], + html: '' + }); + } +}); + +module.exports = TorqueLegendModel; diff --git a/src/geo/map/plain-layer.js b/src/geo/map/plain-layer.js new file mode 100644 index 0000000..d14bf1d --- /dev/null +++ b/src/geo/map/plain-layer.js @@ -0,0 +1,42 @@ +var _ = require('underscore'); +var LayerModelBase = require('./layer-model-base'); + +var ATTRIBUTES_THAT_TRIGGER_VIS_RELOAD = ['color', 'image']; + +/** + * this layer allows to put a plain color or image as layer (instead of tiles) + */ +var PlainLayer = LayerModelBase.extend({ + defaults: { + type: 'Plain', + visible: true, + baseType: 'plain', + className: 'plain', + color: '#FFFFFF', + image: '' + }, + + initialize: function (attrs, options) { + attrs = attrs || {}; + options = options || {}; + if (!options.engine) throw new Error('engine is required'); + + this._engine = options.engine; + + this.bind('change', this._onAttributeChanged, this); + }, + + _onAttributeChanged: function () { + if (_.any(ATTRIBUTES_THAT_TRIGGER_VIS_RELOAD, this.hasChanged, this)) { + this._reload(); + } + }, + + _reload: function () { + this._engine.reload({ + sourceId: this.get('id') + }); + } +}); + +module.exports = PlainLayer; diff --git a/src/geo/map/popup-fields.js b/src/geo/map/popup-fields.js new file mode 100644 index 0000000..d07e095 --- /dev/null +++ b/src/geo/map/popup-fields.js @@ -0,0 +1,19 @@ +var _ = require('underscore'); +var Backbone = require('backbone'); + +var getFieldNames = function (fields) { + return _.map(fields, function (field) { + var o = {}; + o[field.name] = field.title; + return o; + }); +}; + +var PopupFields = Backbone.Collection.extend({ + equals: function (otherFields) { + var myFields = this.toJSON(); + return _.isEqual(getFieldNames(myFields), getFieldNames(otherFields)); + } +}); + +module.exports = PopupFields; diff --git a/src/geo/map/tile-layer.js b/src/geo/map/tile-layer.js new file mode 100644 index 0000000..f552074 --- /dev/null +++ b/src/geo/map/tile-layer.js @@ -0,0 +1,35 @@ +var _ = require('underscore'); +var LayerModelBase = require('./layer-model-base'); + +var ATTRIBUTES_THAT_TRIGGER_VIS_RELOAD = ['urlTemplate']; + +var TileLayer = LayerModelBase.extend({ + defaults: { + type: 'Tiled', + visible: true + }, + + initialize: function (attrs, options) { + attrs = attrs || {}; + options = options || {}; + if (!options.engine) throw new Error('engine is required'); + + this._engine = options.engine; + + this.bind('change', this._onAttributeChanged, this); + }, + + _onAttributeChanged: function () { + if (_.any(ATTRIBUTES_THAT_TRIGGER_VIS_RELOAD, this.hasChanged, this)) { + this._reload(); + } + }, + + _reload: function () { + this._engine.reload({ + sourceId: this.get('id') + }); + } +}); + +module.exports = TileLayer; diff --git a/src/geo/map/tooltip-template.js b/src/geo/map/tooltip-template.js new file mode 100644 index 0000000..4eea1e6 --- /dev/null +++ b/src/geo/map/tooltip-template.js @@ -0,0 +1,48 @@ +var _ = require('underscore'); +var Backbone = require('backbone'); +var PopupFields = require('./popup-fields'); + +var TooltipTemplate = Backbone.Model.extend({ + defaults: { + vertical_offset: 0, + horizontal_offset: 0, + position: 'top|center', + template: '', + alternative_names: { } + }, + + initialize: function (attrs) { + attrs = attrs || {}; + this.fields = new PopupFields(attrs.fields || []); + this.unset('fields'); + }, + + update: function (attrs) { + attrs = _.clone(attrs); + + if (!this.fields.equals(attrs.fields)) { + this.fields.reset(attrs.fields); + } + delete attrs.fields; + + if (attrs.alternative_names) { + attrs.alternative_names = JSON.parse(JSON.stringify(attrs.alternative_names)); + } + + this.set(attrs); + }, + + getFieldNames: function () { + return this.fields.pluck('name'); + }, + + hasFields: function () { + return !this.fields.isEmpty(); + }, + + hasTemplate: function () { + return !!this.get('template'); + } +}); + +module.exports = TooltipTemplate; diff --git a/src/geo/map/torque-layer.js b/src/geo/map/torque-layer.js new file mode 100644 index 0000000..db70701 --- /dev/null +++ b/src/geo/map/torque-layer.js @@ -0,0 +1,224 @@ +var _ = require('underscore'); +var LayerModelBase = require('./layer-model-base'); +var carto = require('carto'); +var Legends = require('./legends/legends'); +var postcss = require('postcss'); +var AnalysisModel = require('../../analysis/analysis-model'); + +var ATTRIBUTES_THAT_TRIGGER_VIS_RELOAD = ['sql', 'sql_wrap', 'source', 'cartocss']; +var TORQUE_LAYER_CARTOCSS_PROPS = [ + '-torque-frame-count', + '-torque-time-attribute', + '-torque-aggregation-function', + '-torque-data-aggregation', + '-torque-resolution' +]; +var LAYER_CARTOCSS_PROPS = [ + 'marker-width' +]; +var LAYER_NAME_IN_CARTO_CSS = '#layer'; +var TORQUE_LAYER_NAME_IN_CARTO_CSS = 'Map'; +var DEFAULT_ANIMATION_DURATION = 30; +var TORQUE_DURATION_ATTRIBUTE = '-torque-animation-duration'; + +/** + * Model for a Torque Layer + */ +var TorqueLayer = LayerModelBase.extend({ + defaults: { + type: 'torque', + visible: true, + isRunning: false, + renderRange: { + start: undefined, + end: undefined + }, + subdomains: [0, 1, 2, 3], + steps: 0, + step: 0, + time: undefined // should be a Date instance + }, + + // Helper method to be used from a few places, it parses torque cartocss to get + // the animation duration or a default duration + getAnimationDuration: function (cartocss) { + var cssTree = postcss().process(cartocss); + var root = cssTree.result.root; + var torqueDuration = DEFAULT_ANIMATION_DURATION; + + root.walkDecls(TORQUE_DURATION_ATTRIBUTE, function (decl) { + torqueDuration = parseInt(decl.value, 10); + }); + + return torqueDuration; + }, + + initialize: function (attrs, options) { + options = options || {}; + if (!options.engine) throw new Error('engine is required'); + + this._engine = options.engine; + + if (attrs.source) { + this.setSource(attrs.source); + } + + this.legends = new Legends(attrs.legends, { engine: this._engine }); + this.unset('legends'); + + this.bind('change', this._onAttributeChanged, this); + + LayerModelBase.prototype.initialize.apply(this, arguments); + }, + + _onAttributeChanged: function () { + var reload = _.any(ATTRIBUTES_THAT_TRIGGER_VIS_RELOAD, function (attr) { + if (this.hasChanged(attr)) { + if (attr === 'cartocss') { + return this.previous('cartocss') && this._torqueCartoCSSPropsChanged(); + } + return true; + } + }, this); + + if (reload) { + this._reload(); + } + }, + + _torqueCartoCSSPropsChanged: function () { + var currentCartoCSS = this.get('cartocss'); + var previousCartoCSS = this.previous('cartocss'); + var renderer = new carto.RendererJS(); + + return !_.isEqual( + this._getTorqueLayerCartoCSSProperties(renderer, currentCartoCSS), + this._getTorqueLayerCartoCSSProperties(renderer, previousCartoCSS) + ); + }, + + _getTorqueLayerCartoCSSProperties: function (renderer, cartoCSS) { + var shader = renderer.render(cartoCSS); + var torqueLayer = shader.findLayer({ name: TORQUE_LAYER_NAME_IN_CARTO_CSS }); + var layer = shader.findLayer({ name: LAYER_NAME_IN_CARTO_CSS }); + + var properties = {}; + _.each(TORQUE_LAYER_CARTOCSS_PROPS, function (property) { + var value = torqueLayer && torqueLayer.eval(property); + if (value) { + properties[property] = value; + } + }); + _.each(LAYER_CARTOCSS_PROPS, function (property) { + var value = layer && layer.eval(property); + if (value) { + properties[property] = value; + } + }); + + return properties; + }, + + _reload: function () { + this._engine.reload({ + sourceId: this.get('id') + }); + }, + + play: function () { + this.set('isRunning', true); + }, + + pause: function () { + this.set('isRunning', false); + }, + + setStep: function (step) { + this.set('step', step); + }, + + renderRange: function (start, end) { + this.set('renderRange', { + start: start, + end: end + }); + }, + + resetRenderRange: function () { + this.set('renderRange', {}); + }, + + isEqual: function (other) { + var properties = ['query', 'query_wrapper', 'cartocss']; + var self = this; + return this.get('type') === other.get('type') && _.every(properties, function (p) { + return other.get(p) === self.get(p); + }); + }, + + getName: function () { + return this.get('layer_name'); + }, + + fetchAttributes: function (layer, featureID, callback) { }, + + // given a timestamp returns a step (float) + timeToStep: function (timestamp) { + var steps = this.get('steps'); + var start = this.get('start'); + var end = this.get('end'); + var step = (steps * (1000 * timestamp - start)) / (end - start); + return step; + }, + + getTileURLTemplates: function () { + return this.get('tileURLTemplates'); + }, + + getSourceId: function () { + var source = this.getSource(); + return source && source.id; + }, + + getSource: function () { + return this.get('source'); + }, + + setSource: function (newSource, options) { + if (this.getSource()) { + this.getSource().unmarkAsSourceOf(this); + } + newSource.markAsSourceOf(this); + this.set('source', newSource, options); + }, + + /** + * Check if an analysis node is the layer's source. + * Only torque and cartodb layers have a source otherwise return false. + */ + hasSource: function (analysisModel) { + return this.getSource().equals(analysisModel); + }, + + update: function (attrs) { + if (attrs.source) { + throw new Error('Use ".setSource" to update a layer\'s source instead of the update method'); + } + LayerModelBase.prototype.update.apply(this, arguments); + }, + + remove: function () { + this.getSource().unmarkAsSourceOf(this); + LayerModelBase.prototype.remove.apply(this, arguments); + } +}, + // Static methods and properties +{ + _checkSourceAttribute: function (source) { + if (!(source instanceof AnalysisModel)) { + throw new Error('Source must be an instance of AnalysisModel'); + } + } +}); + +module.exports = TorqueLayer; diff --git a/src/geo/map/wms-layer.js b/src/geo/map/wms-layer.js new file mode 100644 index 0000000..cf8b23a --- /dev/null +++ b/src/geo/map/wms-layer.js @@ -0,0 +1,20 @@ +var LayerModelBase = require('./layer-model-base'); + +/** + * WMS layer support + */ +var WMSLayer = LayerModelBase.extend({ + defaults: { + type: 'WMS', + visible: true, + service: 'WMS', + request: 'GetMap', + version: '1.1.1', + layers: '', + styles: '', + format: 'image/jpeg', + transparent: false + } +}); + +module.exports = WMSLayer; diff --git a/src/geo/render-modes.js b/src/geo/render-modes.js new file mode 100644 index 0000000..df42c8e --- /dev/null +++ b/src/geo/render-modes.js @@ -0,0 +1,5 @@ +module.exports = { + AUTO: 'auto', + RASTER: 'raster', + VECTOR: 'vector' +}; diff --git a/src/geo/torque-layer-view-base.js b/src/geo/torque-layer-view-base.js new file mode 100644 index 0000000..d909977 --- /dev/null +++ b/src/geo/torque-layer-view-base.js @@ -0,0 +1,211 @@ +var _ = require('underscore'); + +/** + * Implementation of all common logic of a torque-layer view. + * All the methods and attributes here need to be consistent across all implementing models. + * + * Methods are prefixed with _ to indicate that they are not intended to be used outside the implementing models. + */ +var TorqueLayerViewBase = { + setNativeTorqueLayer: function (nativeTorqueLayer) { + var model = this.model; + this.nativeTorqueLayer = nativeTorqueLayer; + if (model.get('visible')) { + this.nativeTorqueLayer.play(); + } + + this.nativeTorqueLayer.on('tilesLoaded', function () { + this.trigger('load'); + }, this); + + this.nativeTorqueLayer.on('tilesLoading', function () { + this.trigger('loading'); + }, this); + + this.nativeTorqueLayer.on('change:time', function (changes) { + this._setModelAttrs({ + step: changes.step, + time: changes.time, + renderRange: { + start: changes.start, + end: changes.end + } + }); + }, this); + + this.nativeTorqueLayer.on('tileError', function (error) { + if (this.showLimitErrors) { + // TODO: Replace type with a new name + this.mapModel.trigger('error:limit', _.extend(error, { type: 'limit' })); + } + }, this); + + this.nativeTorqueLayer.on('change:steps', function (changes) { + this._setModelAttrs({ steps: changes.steps }); + }, this); + + this.nativeTorqueLayer.on('play', function () { + this._callModel('play'); + }, this); + + this.nativeTorqueLayer.on('pause', function () { + this._callModel('pause'); + }, this); + + var steps = model.get('torque-steps') || + (this.nativeTorqueLayer.provider && this.nativeTorqueLayer.provider.getSteps()) || + this.nativeTorqueLayer.options.steps || 0; + + model.set({ + isRunning: this.nativeTorqueLayer.isRunning(), + time: this.nativeTorqueLayer.getTime(), + step: this.nativeTorqueLayer.getStep(), + steps: steps + }); + + this._onModel(); + }, + + _initialAttrs: function (model) { + return { + table: model.get('table_name'), + user: model.get('user_name'), + column: model.get('property'), + blendmode: model.get('torque-blend-mode'), + resolution: 1, + // TODO: manage time columns + countby: 'count(cartodb_id)', + maps_api_template: model.get('maps_api_template'), + client: model.get('client'), + animationDuration: model.get('torque-duration'), + steps: model.get('torque-steps'), + sql: this._getQuery(model), + visible: model.get('visible'), + extra_params: { + api_key: model.get('api_key') + }, + attribution: model.get('attribution'), + cartocss: model.get('cartocss') || model.get('tile_style'), + named_map: model.get('named_map'), + auth_token: model.get('auth_token'), + no_cdn: model.get('no_cdn'), + loop: !(model.get('loop') === false) + }; + }, + + /** + * Set model property but unon changes first in order to not create an infinite loop + */ + _setModelAttrs: function (attrs) { + this._unonModel(); + this.model.set(attrs); + this._onModel(); + }, + + _callModel: function (method) { + this._unonModel(); + var args = Array.prototype.slice.call(arguments, 1); + this.model[method].apply(this.model, args); + this._onModel(); + }, + + _onModel: function () { + this._unonModel(); + this.listenTo(this.model, 'change:isRunning', this._isRunningChanged); + this.listenTo(this.model, 'change:time', this._timeChanged); + this.listenTo(this.model, 'change:step', this._stepChanged); + this.listenTo(this.model, 'change:steps', this._stepsChanged); + this.listenTo(this.model, 'change:renderRange', this._renderRangeChanged); + this.listenTo(this.model, 'change', this._onModelChanged); + this.listenTo(this.model, 'change:cartocss', this._cartoCSSChanged); + this.listenTo(this.model, 'change:customDuration', this._onUpdateDuration); + }, + + _unonModel: function () { + this.stopListening(this.model, 'change:isRunning', this._isRunningChanged); + this.stopListening(this.model, 'change:time', this._timeChanged); + this.stopListening(this.model, 'change:step', this._stepChanged); + this.stopListening(this.model, 'change:steps', this._stepsChanged); + this.stopListening(this.model, 'change:renderRange', this._renderRangeChanged); + this.stopListening(this.model, 'change', this._onModelChanged); + this.stopListening(this.model, 'change:cartocss', this._cartoCSSChanged); + this.stopListening(this.model, 'change:customDuration', this._onUpdateDuration); + }, + + _onUpdateDuration: function () { + var duration = this.model.get('customDuration'); + duration = duration || this.model.getAnimationDuration(this.model.get('cartocss')); + if (duration) { + this.nativeTorqueLayer.animator.duration(duration); + } + }, + + _isRunningChanged: function (m, isRunning) { + if (isRunning) { + this.nativeTorqueLayer.play(); + } else { + this.nativeTorqueLayer.pause(); + } + }, + + _timeChanged: function (m, time) { + this.nativeTorqueLayer.setStep(this.nativeTorqueLayer.timeToStep(time)); + }, + + _stepChanged: function (m, step) { + this.nativeTorqueLayer.setStep(step); + }, + + _stepsChanged: function (m, steps) { + this.nativeTorqueLayer.setSteps(steps); + }, + + _cartoCSSChanged: function (m, cartocss) { + this.nativeTorqueLayer.setCartoCSS(this.model.get('cartocss')); + this._onUpdateDuration(); + }, + + _renderRangeChanged: function (m, r) { + if (_.isObject(r) && _.isNumber(r.start) && _.isNumber(r.end)) { + this.nativeTorqueLayer.renderRange(r.start, r.end); + } else { + this.nativeTorqueLayer.resetRenderRange(); + } + }, + + // TODO: This could be "exploded" into different bindings / methods + _onModelChanged: function () { + if (!this.model.hasChanged()) return; + + if (this.model.hasChanged('visible')) { + this.model.get('visible') ? this.nativeTorqueLayer.show() : this.nativeTorqueLayer.hide(); + } + + if (this.model.hasChanged('tileURLTemplates')) { + // REAL HACK + this.nativeTorqueLayer.provider.templateUrl = this.model.getTileURLTemplates()[0]; + this.nativeTorqueLayer.provider.options.subdomains = this.model.get('subdomains'); + _.extend(this.nativeTorqueLayer.provider.options, this.model.get('meta')); + // this needs to be deferred in order to break the infinite loop + // of setReady changing keys and keys updating the model + // If we do this in the next iteration 'urls' will not be in changedAttributes + // so this will not pass through this code + setTimeout(function () { + this.nativeTorqueLayer.provider._setReady(true); + this.nativeTorqueLayer._reloadTiles(); + }.bind(this), 0); + } + }, + + _getQuery: function (layerModel) { + var source = layerModel.get('source'); + var query = source ? source.get('query') : 'select * from ' + layerModel.get('table_name'); + var qw = layerModel.get('sql_wrap'); + if (qw) { + query = _.template(qw)({ sql: query }); + } + return query; + } +}; + +module.exports = TorqueLayerViewBase; diff --git a/src/geo/ui/attribution/attribution-template.tpl b/src/geo/ui/attribution/attribution-template.tpl new file mode 100644 index 0000000..8d10d1a --- /dev/null +++ b/src/geo/ui/attribution/attribution-template.tpl @@ -0,0 +1,4 @@ +
+ +

<%= attributions %>

+
diff --git a/src/geo/ui/attribution/attribution-view.js b/src/geo/ui/attribution/attribution-view.js new file mode 100644 index 0000000..d224855 --- /dev/null +++ b/src/geo/ui/attribution/attribution-view.js @@ -0,0 +1,98 @@ +var $ = require('jquery'); +var _ = require('underscore'); +var View = require('../../../core/view'); +var Model = require('../../../core/model'); +var template = require('./attribution-template.tpl'); +var Sanitize = require('../../../core/sanitize'); + +/** + * Attribution overlay + * + */ + +module.exports = View.extend({ + className: 'CDB-Attribution', + + events: { + 'click .js-button': '_toggleAttributions', + 'dblclick': 'killEvent' + }, + + initialize: function () { + this.model = new Model({ + visible: false + }); + this.map = this.options.map; + + this._onDocumentKeyDown = this._onDocumentKeyDown.bind(this); + this._toggleAttributionsBasedOnContainer = this._toggleAttributionsBasedOnContainer.bind(this); + + this._initBinds(); + }, + + render: function () { + var attributions = _.compact(this.map.get('attribution')).join(', '); + var isGMaps = this.map.get('provider') !== 'leaflet'; + this.$el.html( + template({ + attributions: Sanitize.html(attributions) + }) + ); + this.$el.toggleClass('CDB-Attribution--gmaps', !!isGMaps); + this._toggleAttributionsBasedOnContainer(); + + return this; + }, + + _initBinds: function () { + this.model.bind('change:visible', function (mdl, isVisible) { + this[ isVisible ? '_showAttributions' : '_hideAttributions' ](); + }, this); + this.map.bind('change:attribution', this.render, this); + this.add_related_model(this.map); + $(window).bind('resize', this._toggleAttributionsBasedOnContainer); + }, + + _enableDocumentBinds: function () { + $(document).bind('keydown', this._onDocumentKeyDown); + }, + + _disableDocumentBinds: function () { + $(document).unbind('keydown', this._onDocumentKeyDown); + }, + + _onDocumentKeyDown: function (ev) { + if (ev && ev.keyCode === 27) { + this._toggleAttributions(); + } + }, + + _showAttributions: function () { + this.$el.addClass('is-active'); + this._enableDocumentBinds(); + }, + + _hideAttributions: function () { + this.$el.removeClass('is-active'); + this._disableDocumentBinds(); + }, + + _toggleAttributions: function () { + this.model.set('visible', !this.model.get('visible')); + }, + + _toggleAttributionsBasedOnContainer: function () { + var MAP_CONTAINER_MOBILE_WIDTH = 650; + if (this.map.getMapViewSize().x > MAP_CONTAINER_MOBILE_WIDTH) { + this.model.set('visible', true); + } else { + this.model.set('visible', false); + } + }, + + clean: function () { + this._disableDocumentBinds(); + $(window).unbind('resize', this._toggleAttributionsBasedOnContainer); + View.prototype.clean.call(this); + } +}); diff --git a/src/geo/ui/default-infowindow-template.tpl b/src/geo/ui/default-infowindow-template.tpl new file mode 100644 index 0000000..7939351 --- /dev/null +++ b/src/geo/ui/default-infowindow-template.tpl @@ -0,0 +1,25 @@ +
+
+
+ <% if (typeof loading !== 'undefined' && loading) { %> +
+ <% } %> +
+
+
    + <% if (content.fields) { %> + <% _.each(content.fields, function (field) { %> +
  • + <% if (field.title) { %>
    <%- field.title %>
    <% } %> + <% if (field.value) { %>

    <%- field.value %>

    <% } %> +
  • + <% }) %> + <% } %> +
+
+
+
+
+
+
+
diff --git a/src/geo/ui/infowindow-model.js b/src/geo/ui/infowindow-model.js new file mode 100644 index 0000000..5fe8f32 --- /dev/null +++ b/src/geo/ui/infowindow-model.js @@ -0,0 +1,154 @@ +var _ = require('underscore'); +var Backbone = require('backbone'); +var defaultInfowindowTemplate = require('./default-infowindow-template.tpl'); + +var InfowindowModel = Backbone.Model.extend({ + defaults: { + offset: [28, 0], // offset of the tip calculated from the bottom left corner + maxHeight: 180, // max height of the content, not the whole infowindow + autoPan: true, + template_type: 'mustache', + content: '', + alternative_names: { }, + visibility: false + }, + + DEFAULT_TEMPLATE: defaultInfowindowTemplate, + + TEMPLATE_ATTRIBUTES: ['template', 'template_type', 'alternative_names', 'width', 'maxHeight', 'offset'], + + initialize: function (attrs) { + attrs = attrs || {}; + this._fields = new Backbone.Collection(attrs.fields || []); + this.unset('fields', { silent: true }); + + // Set a default template + if (!this._hasTemplate()) { + this.set({ + template: this.DEFAULT_TEMPLATE, + template_type: 'underscore' + }); + } + }, + + _hasTemplate: function () { + return this.get('template') && this.get('template').trim && this.get('template').trim(); + }, + + updateContent: function (attributes, options) { + options = options || {}; + options = _.pick(options, 'showEmptyFields'); + var fields = this._fields.toJSON(); + this.set('content', InfowindowModel.contentForFields(attributes, fields, options)); + }, + + setInfowindowTemplate: function (infowindowTemplateModel) { + var attrs = _.pick(infowindowTemplateModel.toJSON(), this.TEMPLATE_ATTRIBUTES); + // Remove keys that have a falsy value + attrs = _.pick(attrs, _.identity); + this.set(_.clone(attrs)); + this._fields.reset(infowindowTemplateModel.fields.toJSON()); + this._infowindowTemplateModel = infowindowTemplateModel; + }, + + setLoading: function () { + this.set({ + content: { + fields: [{ + type: 'loading', + title: 'loading', + value: '…' + }] + } + }); + return this; + }, + + setError: function () { + this.set({ + content: { + fields: [{ + title: null, + alternative_name: null, + value: 'There has been an error...', + index: null, + type: 'error' + }], + data: {} + } + }); + + return this; + }, + + setLatLng: function (latLng) { + this.set('latlng', latLng); + }, + + show: function () { + this.set('visibility', true); + }, + + hide: function () { + this.set('visibility', false); + }, + + isVisible: function () { + return !!this.get('visibility'); + }, + + setCurrentFeatureId: function (featureId) { + this.set('currentFeatureId', featureId); + }, + + unsetCurrentFeatureId: function () { + this.unset('currentFeatureId'); + }, + + getCurrentFeatureId: function (featureId) { + return this.get('currentFeatureId'); + }, + + getAlternativeName: function (fieldName) { + return this.get('alternative_names') && this.get('alternative_names')[fieldName]; + }, + + hasInfowindowTemplate: function (infowindowTemplateModel) { + return this._infowindowTemplateModel && this._infowindowTemplateModel === infowindowTemplateModel; + } +}, { + contentForFields: function (attributes, fields, options) { + options = options || {}; + var renderFields = []; + + for (var j = 0; j < fields.length; ++j) { + var field = fields[j]; + var value = attributes[field.name]; + if (options.showEmptyFields || (value !== undefined && value !== null)) { + renderFields.push({ + name: field.name, + title: field.title ? field.name : null, + value: (value !== undefined && value !== null) ? value : null, + index: j + }); + } + } + + // manage when there is no data to render + if (renderFields.length === 0) { + renderFields.push({ + title: null, + value: 'No data available', + index: 0, + type: 'empty' + }); + } + + return { + fields: renderFields, + data: attributes + }; + } +}); + +module.exports = InfowindowModel; diff --git a/src/geo/ui/infowindow-view.js b/src/geo/ui/infowindow-view.js new file mode 100644 index 0000000..1298a77 --- /dev/null +++ b/src/geo/ui/infowindow-view.js @@ -0,0 +1,676 @@ +var _ = require('underscore'); +var $ = require('jquery'); +var Ps = require('perfect-scrollbar'); +require('clip-path-polygon'); +var sanitize = require('../../core/sanitize'); +var Template = require('../../core/template'); +var View = require('../../core/view'); +var util = require('../../core/util'); + +var ESC_KEY = 27; + +/** + * Usage: + * var infowindow = new Infowindow({ + * model: infowindowModel, + * mapView: mapView + * }); + * + * // Show the infowindow: + * infowindow.showInfowindow(); + */ + +var Infowindow = View.extend({ + options: { + imageTransitionSpeed: 300, + hookMargin: 24, + hookHeight: 16 + }, + + className: 'CDB-infowindow-wrapper', + + events: { + // Close bindings + 'click .js-close': '_closeInfowindow', + // some migrations doesn't have js-close class + 'click .close': '_closeInfowindow', + 'touchstart .js-close': '_closeInfowindow', + 'MSPointerDown .js-close': '_closeInfowindow', + // Rest infowindow bindings + 'dragstart': '_stopPropagation', + 'mousedown': '_stopPropagation', + 'pointerdown': '_stopPropagation', + 'touchstart': '_stopPropagation', + 'MSPointerDown': '_stopPropagation', + 'dblclick': '_stopPropagation', + 'DOMMouseScroll': 'killEvent', + 'MozMousePixelScroll': 'killEvent', + 'mousewheel': 'killEvent', + 'dbclick': '_stopPropagation', + 'click': '_stopPropagation' + }, + + initialize: function () { + this.mapView = this.options.mapView; + + this._compileTemplate(); + this._initBinds(); + + // Hide the element + this.$el.hide(); + }, + + /** + * Render infowindow content + */ + render: function () { + this.clearSubViews(); + this.$el.empty(); + + if (this.template) { + // Clone fields and template name + var fields = _.map(this.model.attributes.content.fields, function (field) { + return _.clone(field); + }); + + var data = this.model.get('content') ? this.model.get('content').data : {}; + + // Sanitized fields + fields = _.map(fields, this._sanitizeField, this); + + // Join plan fields values with content to work with + // custom infowindows and CartoDB infowindows. + var values = {}; + + _.each(fields, function (pair) { + values[pair.name] = pair.value; + }); + + var obj = _.extend({ + content: { + fields: fields, + data: data + } + }, values); + + this.$el.html( + sanitize.html(this.template(obj), this.model.get('sanitizeTemplate')) + ); + + // Set width and max-height from the model only + // If there is no width set, we don't force our infowindow + if (this.model.get('width')) { + this.$('.js-infowindow').css('width', this.model.get('width') + 'px'); + } + + if (this.model.get('maxHeight')) { + this._getContent().css('max-height', this.model.get('maxHeight') + 'px'); + } + + if (this._containsCover()) { + this._loadCover(); + } + + this._setupClasses(); + this._renderScroll(); + this._renderShadows(); + this._bindScroll(); + } + + return this; + }, + + _initBinds: function () { + _.bindAll(this, '_onKeyUp', '_onLoadImageSuccess', '_onLoadImageError'); + + this.listenTo(this.model, 'change:content change:alternative_names change:width change:maxHeight', this.render, this); + this.listenTo(this.model, 'change:latlng', this._updateAndAdjustPan, this); + this.listenTo(this.model, 'change:visibility', this.toggle, this); + this.listenTo(this.model, 'change:template change:sanitizeTemplate', this._compileTemplate, this); + + this.listenTo(this.mapView.map, 'change', this._updatePosition, this); + + this.listenTo(this.mapView, 'zoomstart', function () { + this.hide(true); + }); + + this.listenTo(this.mapView, 'zoomend', function () { + this.show(); + }); + }, + + // migration issue: some infowindows doesn't have this selector + _getContent: function () { + var $el = this.$('.js-content'); + if ($el.length === 0) { + $el = this.$('.cartodb-popup-content'); + } + + return $el; + }, + + _onKeyUp: function (event) { + if (event && event.keyCode === ESC_KEY) { + this._closeInfowindow(); + } + }, + + _setupClasses: function () { + var $infowindow = this.$('.js-infowindow'); + var hasHeader = this.$('.js-header').length; + var hasCover = this.$('.js-cover').length; + var hasContent = this._getContent().length; + var hasTitle = this.$('.CDB-infowindow-title').length; + var numberOfFields = this.model.get('content') && this.model.get('content').fields.length; + + if (hasCover) { + $infowindow + .addClass('has-header-image') + .toggleClass('no-content', numberOfFields < 3); + } + + if (hasHeader) { + $infowindow.addClass('has-header'); + } + + if (hasContent) { + $infowindow.addClass('has-fields'); + } + + if (hasTitle) { + $infowindow.addClass('has-title'); + } + }, + + _renderScroll: function () { + if (this._getContent().length === 0) { + return; + } + + this._content = this._getContent().get(0); + this.$('.js-infowindow').addClass('has-scroll'); + + Ps.initialize(this._content, { + wheelSpeed: 1, + wheelPropagation: true, + minScrollbarLength: 20 + }); + }, + + _renderShadows: function () { + this.$shadowTop = $('
').addClass('CDB-infowindow-canvasShadow CDB-infowindow-canvasShadow--top'); + this.$shadowBottom = $('
').addClass('CDB-infowindow-canvasShadow CDB-infowindow-canvasShadow--bottom'); + var $inner = this.$('.js-inner'); + $inner.append(this.$shadowTop); + $inner.append(this.$shadowBottom); + _.defer(function () { + this._showOrHideShadows(); + }.bind(this)); + }, + + _bindScroll: function () { + this.$(this._content) + .on('ps-y-reach-start', _.bind(this._onScrollTop, this)) + .on('ps-y-reach-end', _.bind(this._onScrollBottom, this)) + .on('ps-scroll-y', _.bind(this._onScroll, this)); + }, + + _onScrollTop: function () { + this.$shadowTop.removeClass('is-visible'); + }, + + _onScroll: function () { + this._showOrHideShadows(); + }, + + _showOrHideShadows: function () { + var $el = $(this._content); + if ($el.length) { + var currentPos = $el.scrollTop(); + var max = $el.get(0).scrollHeight; + var height = $el.outerHeight(); + var maxPos = max - height; + + this.$shadowTop.toggleClass('is-visible', currentPos > 0); + this.$shadowBottom.toggleClass('is-visible', currentPos < maxPos); + } + }, + + _onScrollBottom: function () { + this.$shadowBottom.removeClass('is-visible'); + }, + + _container: function () { + return this.el.querySelector('.js-container'); + }, + + /** + * Compile template of the infowindow + */ + _compileTemplate: function () { + var template = this.model.get('template'); + + if (typeof (template) !== 'function') { + this.template = new Template({ + template: template, + type: this.model.get('template_type') || 'mustache' + }).asFunction(); + } else { + this.template = template; + } + + this.render(); + }, + + _sanitizeValue: function (key, val) { + if (_.isObject(val)) { + return val; + } + + return String(val); + }, + + _sanitizeField: function (attr) { + // Check null or undefined :| and set both to empty == '' + if (attr.value === null || attr.value === undefined) { + attr.value = ''; + } + + // Get the alternative name + var alternativeName = this.model.getAlternativeName(attr.name); + attr.title = (attr.title && alternativeName) ? alternativeName : attr.title; + + // Save new sanitized value + attr.value = JSON.parse(JSON.stringify(attr.value), this._sanitizeValue); + + return attr; + }, + + /** + * Does header contain cover? + */ + _containsCover: function () { + return !!this.$('.js-infowindow').attr('data-cover'); + }, + + _containsTemplateCover: function () { + return this.$('.js-cover img').length > 0; + }, + + _getCoverURL: function () { + var content = this.model.get('content'); + var imageSRC = this.$('.js-cover img').attr('src'); + + if (imageSRC) { + return imageSRC; + } + + if (content && content.fields && content.fields.length > 0) { + return (content.fields[0].value || '').toString(); + } + + return false; + }, + + _imageOnHookNotSupported: function () { + var noCssClipPath = util.ie || util.browser.ie || util.browser.edge; + return noCssClipPath; + }, + + _loadImageHook: function (imageDimensions, coverDimensions, url) { + var $hook = this.$('.CDB-hook'); + if (!$hook) { + return; + } + + if (this._imageOnHookNotSupported()) { + $hook.hide(); + return; + } + + var $hookImage = $('') + .addClass('CDB-hookImage js-hookImage') + .attr('src', url); + + $hook.append($hookImage); + + $hookImage.css({ + marginTop: -(imageDimensions.height - this.options.hookHeight), + width: coverDimensions.width, + display: 'none' + }); + + $hookImage.load( + function () { + $hook.parent().addClass('has-image'); + $hookImage.clipPath(this._getHookPoints(imageDimensions.height - this.options.hookHeight)); + $hookImage.show(); + }.bind(this) + ); + }, + + _getHookPoints: function (imageHeight) { + return [ + [24, imageHeight], + [24, imageHeight + 16], + [48, imageHeight], + [24, imageHeight] + ]; + }, + + _loadCoverFromTemplate: function (url) { + this.$('.js-cover img').remove(); + this._loadCoverFromUrl(url); + }, + + _loadCoverFromUrl: function (url) { + var $cover = this.$('.js-cover'); + + this._startCoverLoader(); + + var $img = $(""); + $cover.append($img); + + $img + .load(this._onLoadImageSuccess) + .error(this._onLoadImageError) + .attr('src', url); + }, + + _onLoadImageError: function () { + this._stopCoverLoader(); + this._showInfowindowImageError(); + }, + + _onLoadImageSuccess: function () { + var $cover = this.$('.js-cover'); + var $img = this.$('.CDB-infowindow-media-item'); + var url = $img.attr('src'); + var numFields = this.model.get('content').fields.length; + + var imageDimensions = { width: $img.width(), height: $img.height() }; + var coverDimensions = { width: $cover.width(), height: $cover.height() }; + + var styles = this._calcImageStyle(imageDimensions, coverDimensions); + + $img.css(styles); + + $cover.css({ height: imageDimensions.height - this.options.hookHeight }); + + this._stopCoverLoader(); + + $img.fadeIn(150); + + if (numFields < 3 && imageDimensions.height >= this.$el.height()) { + this._loadImageHook(imageDimensions, coverDimensions, url); + } + }, + + _calcImageStyle: function (imageDimensions, coverDimensions) { + var styles = {}; + + var imageRatio = imageDimensions.height / imageDimensions.width; + var coverRatio = coverDimensions.height / coverDimensions.width; + + if (imageDimensions.width > coverDimensions.width && imageDimensions.height > coverDimensions.height) { + if (imageRatio < coverRatio) { + styles = { height: coverDimensions.height }; + } + } else { + styles = { width: imageDimensions.width }; + } + + return styles; + }, + + _loadCover: function () { + this._renderCoverLoader(); + this._startCoverLoader(); + + var url = this._getCoverURL(); + + if (this._isLoadingFields()) { + return; + } + + if (!this._isValidURL(url)) { + this._stopCoverLoader(); + this._showInfowindowImageError(); + return; + } + + if (this._containsTemplateCover()) { + this._loadCoverFromTemplate(url); + } else { + this._loadCoverFromUrl(url); + } + }, + + _isLoadingFields: function () { + var content = this.model.get('content'); + return !content || (content.fields.length === 1 && content.fields[0].type === 'loading'); + }, + + _renderCoverLoader: function () { + var $loader = $('
').addClass('CDB-Loader js-loader'); + + if (this.$('.js-cover').length > 0) { + this.$('.js-cover').append($loader); + } + }, + + _startCoverLoader: function () { + this.$('.js-infowindow').addClass('is-loading'); + this.$('.js-loader').addClass('is-visible'); + }, + + _clearInfowindowImageError: function () { + this.$('.js-infowindow').removeClass('is-fail'); + }, + + _showInfowindowImageError: function () { + this.$('.js-infowindow').addClass('is-fail'); + this.$('.js-cover').append('

Non-valid picture URL

'); + }, + + _stopCoverLoader: function () { + this.$('.js-infowindow').removeClass('is-loading'); + this.$('.js-loader').removeClass('is-visible'); + }, + + /** + * Return true if the provided URL is valid + */ + _isValidURL: function (url) { + if (url) { + var urlPattern = /^(http|ftp|https):\/\/[\w-]+(\.[\w-]+)+([\w.,@?^=%&:\/~+#-|]*[\w@?^=%&\/~+#-])?$/; + return String(url).match(urlPattern) !== null; + } + + return false; + }, + + /** + * Toggle infowindow visibility + */ + toggle: function () { + if (this.model.get('visibility')) { + this.show(true); + } else { + this.hide(); + } + }, + + _stopPropagation: function (ev) { + ev.stopPropagation(); + }, + + /** + * Close infowindow + */ + _closeInfowindow: function (ev) { + if (ev) { + ev.preventDefault(); + ev.stopImmediatePropagation(); + } + if (this.model.get('visibility')) { + this.model.set('visibility', false); + } + }, + + /** + * Show infowindow (update, pan, etc) + */ + show: function (adjustPan) { + $(document) + .off('keyup', this._onKeyUp) + .on('keyup', this._onKeyUp); + + if (this.model.get('visibility')) { + this.$el.css({ left: -5000 }); + this._update(adjustPan); + } + }, + + /** + * Get infowindow visibility + */ + isHidden: function () { + return !this.model.get('visibility'); + }, + + /** + * Set infowindow to hidden + */ + hide: function (force) { + $(document).off('keyup', this._onKeyUp); + if (force || !this.model.get('visibility')) this._animateOut(); + }, + + _updateAndAdjustPan: function () { + this._update(true); + }, + + /** + * Update infowindow + */ + _update: function (adjustPan) { + if (!this.isHidden()) { + var delay = 0; + + if (adjustPan) { + delay = this.adjustPan(); + } + + this._updatePosition(); + this._animateIn(delay); + } + }, + + /** + * Animate infowindow to show up + */ + _animateIn: function (delay) { + if (!util.ie || (util.browser.ie && util.browser.ie.version > 8)) { + this.$el.css({ + 'marginBottom': '-10px', + 'display': 'block', + 'visibility': 'visible', + opacity: 0 + }); + + this.$el + .delay(delay) + .animate({ + opacity: 1, + marginBottom: 0 + }, 300); + } else { + this.$el.show(); + } + }, + + /** + * Animate infowindow to disappear + */ + _animateOut: function () { + if (!util.ie || (util.browser.ie && util.browser.ie.version > 8)) { + var self = this; + this.$el.animate({ + marginBottom: '-10px', + opacity: '0', + display: 'block' + }, 180, function () { + self.$el.css({visibility: 'hidden'}); + }); + } else { + this.$el.hide(); + } + }, + + /** + * Adjust pan to show correctly the infowindow + */ + // TODO: This can be private + adjustPan: function () { + var offset = this.model.get('offset'); + + if (!this.model.get('autoPan') || this.isHidden()) { return; } + + var containerHeight = this.$el.outerHeight(true) + 15; // Adding some more space + var containerWidth = this.$el.width(); + var pos = this.mapView.latLngToContainerPoint(this.model.get('latlng')); + var adjustOffset = {x: 0, y: 0}; + var size = this.mapView.getSize(); + var waitCallback = 0; + + if (pos.x - offset[0] < 0) { + adjustOffset.x = pos.x - offset[0] - 10; + } + + if (pos.x - offset[0] + containerWidth > size.x) { + adjustOffset.x = pos.x + containerWidth - size.x - offset[0] + 10; + } + + if (pos.y - containerHeight < 0) { + adjustOffset.y = pos.y - containerHeight - 10; + } + + if (pos.y - containerHeight > size.y) { + adjustOffset.y = pos.y + containerHeight - size.y; + } + + if (adjustOffset.x || adjustOffset.y) { + this.mapView.panBy(adjustOffset); + waitCallback = 300; + } + + return waitCallback; + }, + + /** + * Update the position (private) + */ + _updatePosition: function () { + if (this.isHidden()) { + return; + } + + var offset = this.model.get('offset'); + var pos = this.mapView.latLngToContainerPoint(this.model.get('latlng')); + var left = pos.x - offset[0]; + var size = this.mapView.getSize(); + var bottom = -1 * (pos.y - offset[1] - size.y); + + this.$el.css({ bottom: bottom, left: left }); + }, + + /** + * Set visibility infowindow + */ + showInfowindow: function () { + this.model.set('visibility', true); + } +}); + +module.exports = Infowindow; diff --git a/src/geo/ui/legends/base/img-loader-view.js b/src/geo/ui/legends/base/img-loader-view.js new file mode 100644 index 0000000..3be1ac6 --- /dev/null +++ b/src/geo/ui/legends/base/img-loader-view.js @@ -0,0 +1,85 @@ +var $ = require('jquery'); +var Backbone = require('backbone'); +var util = require('../../../../core/util'); + +module.exports = Backbone.View.extend({ + initialize: function (opts) { + if (!opts.el) { throw new Error('element is mandatory.'); } + if (!opts.imageClass) { throw new Error('Image class is mandatory.'); } + + this._color = this.$el.data('color'); + this._icon = this.$el.data('icon'); + + this._imageClass = opts.imageClass; + this._lastImage = { + url: null, + content: null + }; + }, + + _loadImage: function () { + var self = this; + var isSVG = this._isSVG(this._icon); + + if (this.$el.length === 0) { + return; + } + + if (isSVG) { + this._requestImage(this._icon, function (content) { + var svg = content.cloneNode(true); + var $svg = $(svg); + $svg = $svg.removeAttr('xmlns:a'); + $svg.attr('class', self._imageClass + ' js-image'); + + self.$el.empty().append($svg); + + $svg.css('fill', self._color); + $svg.find('g').css('fill', 'inherit'); + $svg.find('path').css('fill', 'inherit'); + $svg.find('rect').each(function (_, rect) { + var $rect = $(rect); + if ($rect.css('fill') !== 'none') { + $rect.css('fill', 'inherit'); + } + }); + }); + } else { + var $img = $(''); + $img.attr('class', self._imageClass + ' js-image'); + $img.attr('src', this._icon + '?req=markup'); + this.$el.empty().append($img); + } + }, + + _requestImage: function (url, callback) { + var self = this; + var completeUrl = url + '?req=ajax'; + + if (this._lastImage.url === completeUrl) { + callback && callback(this._lastImage.content); + } else { + $.ajax(completeUrl) + .done(function (data) { + self._lastImage.url = completeUrl; + var content = self._lastImage.content = data.getElementsByTagName('svg')[0]; + callback && callback(content); + }) + .fail(function () { + throw new Error("Couldn't get " + completeUrl + ' file.'); + }); + } + }, + + _updateImageColor: function (color) { + this.$('.js-image').css('fill', color); + }, + + _isSVG: function (url) { + if (!url) { + return false; + } + var noQueryString = url.split('?')[0]; + return noQueryString && util.endsWith(noQueryString.toUpperCase(), 'SVG'); + } +}); diff --git a/src/geo/ui/legends/base/legend-title.tpl b/src/geo/ui/legends/base/legend-title.tpl new file mode 100644 index 0000000..dddefa1 --- /dev/null +++ b/src/geo/ui/legends/base/legend-title.tpl @@ -0,0 +1,3 @@ +

+ <%- title %> +

diff --git a/src/geo/ui/legends/base/legend-view-base.js b/src/geo/ui/legends/base/legend-view-base.js new file mode 100644 index 0000000..94e54dc --- /dev/null +++ b/src/geo/ui/legends/base/legend-view-base.js @@ -0,0 +1,107 @@ +var _ = require('underscore'); +var Backbone = require('backbone'); +var sanitize = require('../../../../core/sanitize'); +var legendTitleTemplate = require('./legend-title.tpl'); +var ImageLoaderView = require('./img-loader-view'); + +var LegendViewBase = Backbone.View.extend({ + + className: 'CDB-Legend-item u-tSpace-xl', + + initialize: function (opts) { + this._placeholderTemplate = opts.placeholderTemplate; + + this.model.on('change', this.render, this); + }, + + render: function () { + this.$el.html(this._generateHTML()); + + this._loadImages(); + + this.model.isVisible() + ? this.$el.show() + : this.$el.hide(); + + this._toggleLoadingClass(); + + return this; + }, + + _generateHTML: function () { + if (this.model.isSuccess() && this.model.isAvailable()) { + return this._getLegendHTML(); + } + + var placeholder = this._getPlaceholderHTML(); + + if (this.model.isLoading()) { + return placeholder; + } + + var message = this.model.isError() + ? this._generateTitle({ title: 'Something went wrong', error: 'error' }) + : this._generateTitle({ title: 'No data available', error: 'alert' }); + + return [message, placeholder].join('\n'); + }, + + _getLegendHTML: function () { + var html = []; + + // Legend title + if (this.model.get('title')) { + html.push(this._generateTitle({ title: this.model.get('title') })); + } + + // Pre HTML Snippet + if (this.model.get('preHTMLSnippet')) { + html.push(this._sanitize(this.model.get('preHTMLSnippet'))); + } + + // Template + html.push(this._sanitize(this._getCompiledTemplate())); + + // Post HTML Snippet + if (this.model.get('postHTMLSnippet')) { + html.push(this._sanitize(this.model.get('postHTMLSnippet'))); + } + + return html.join('\n'); + }, + + _generateTitle: function (data) { + return legendTitleTemplate({ title: data.title, error: data.error }); + }, + + _getPlaceholderHTML: function () { + return (this._placeholderTemplate && this._placeholderTemplate()) || ''; + }, + + _getCompiledTemplate: function () { + throw new Error('Subclasses of LegendViewBase must implement _getCompiledTemplate'); + }, + + _afterRender: function () { }, + + _sanitize: function (html) { + return sanitize.html(html); + }, + + _toggleLoadingClass: function () { + this.$el.toggleClass('is-loading', this.model.isLoading()); + }, + + _loadImages: function () { + _.each(this.$('.js-image-container'), function ($el) { + var iconView = new ImageLoaderView({ + el: $el, + imageClass: 'Legend-fillImageAsset' + }); + + iconView._loadImage(); + }); + } +}); + +module.exports = LegendViewBase; diff --git a/src/geo/ui/legends/bubble/legend-template.tpl b/src/geo/ui/legends/bubble/legend-template.tpl new file mode 100644 index 0000000..c2f6916 --- /dev/null +++ b/src/geo/ui/legends/bubble/legend-template.tpl @@ -0,0 +1,37 @@ +
+
    + <% for (var i in bubbleSizes) { %> + <% + var customCssClass = ''; + var index = +i; + if (hasCustomLabels) { + customCssClass = (labels[0] !== '' && index === 0) ? '' : 'Bubble-item--custom'; + } + %> +
  • + <% if (!hasCustomLabels || (hasCustomLabels && index === 0)) {%> +
    +
    <%= formatter.formatNumber(labels[i]) %>
    +
    + <% } %> +
    + +
    +
  • + <% } %> + + <% if (labels[labels.length - 1]) { %> +
  • +
    +
    <%= formatter.formatNumber(labels[labels.length - 1]) %>
    +
    +
  • + <% } %> +
+ + <% if (!hasCustomLabels) { %> +

+ AVG: <%= formatter.formatNumber(avgLabel) %> +

+ <% } %> +
diff --git a/src/geo/ui/legends/bubble/legend-view.js b/src/geo/ui/legends/bubble/legend-view.js new file mode 100644 index 0000000..d3b3a34 --- /dev/null +++ b/src/geo/ui/legends/bubble/legend-view.js @@ -0,0 +1,112 @@ +var _ = require('underscore'); +var LegendViewBase = require('../base/legend-view-base'); +var template = require('./legend-template.tpl'); +var formatter = require('../../../../util/formatter'); + +var BubbleLegendView = LegendViewBase.extend({ + + className: 'CDB-Legend-item u-tSpace-xl u-clearfix', + + _getCompiledTemplate: function () { + return template({ + hasCustomLabels: this._hasCustomLabels(), + labels: this._calculateLabels(), + bubbleSizes: this._calculateBubbleSizes(), + labelPositions: this._calculateLabelPositions(), + avgSize: this._calculateAverageSize(), + avgLabel: this.model.get('avg'), + fillColor: this.model.get('fillColor'), + formatter: formatter + }); + }, + + _hasCustomLabels: function () { + var topLabel = this.model.get('topLabel'); + var bottomLabel = this.model.get('bottomLabel'); + return ((topLabel != null && topLabel !== '') || (bottomLabel != null && bottomLabel !== '')); + }, + + _calculateLabelPositions: function () { + var labelPositions; + if (this._hasCustomLabels()) { + labelPositions = [0, 100]; + } else { + labelPositions = this._calculateBubbleSizes(); + labelPositions.push(0); + } + return labelPositions; + }, + + _calculateLabels: function () { + var labels; + + if (this._hasCustomLabels()) { + labels = []; + labels.push(this.model.get('bottomLabel')); + labels.push(this.model.get('topLabel')); + labels = labels.reverse(); + } else { + labels = this.model.get('values').slice(0); + if (this._areSizesInAscendingOrder()) { + labels = labels.reverse(); + } + } + return labels; + }, + + _calculateValues: function () { + var sizes = this.model.get('sizes').slice(0); + if (this._areSizesInAscendingOrder()) { + sizes = sizes.reverse(); + } + return sizes; + }, + + _areSizesInAscendingOrder: function () { + var sizes = this.model.get('sizes').slice(0); + return _.first(sizes) < _.last(sizes); + }, + + _calculateBubbleSizes: function () { + var sizes = this._calculateValues(); + var maxSize = _.max(sizes); + return _.map(sizes, function (size, index) { + return size * 100 / maxSize; + }); + }, + + _calculateAverageSize: function () { + // we build the size range, and reverse it to be able to normalize it with values. + var sizes = this._calculateBubbleSizes().reverse(); + + // we put 0 as the initial of the range + sizes.unshift(0); + + // we need values and sizes because avg is a value and we need to place it taking sizes as reference. + var values = this.model.get('values').slice(0); + var avg = this.model.get('avg'); + + // we need to position it in the right range + // this reduce thing search the index of the range (index base) that avg belongs to + // while avg is greater than the current value, memo is the next index, if not, memo doesn't change anymore + // index + 1 because we pushed 0 to sizes and we need both arrays to have the same length + var index = _.reduce(values, function (memo, value, index) { + return value > avg ? memo : index + 1; + }, 0); + + // avg it's at least equal to the last item + if (index === values.length) { + return 100; + } + + // with the index calculate above, we get the % of the sizes we have to place the avg + var minValue = sizes[index - 1]; + var maxValue = sizes[index]; + + // Inside the range, we position it lineal + var offset = (avg - values[index - 1]) / (values[index] - values[index - 1]); + return minValue + (maxValue - minValue) * offset; + } +}); + +module.exports = BubbleLegendView; diff --git a/src/geo/ui/legends/bubble/placeholder-template.tpl b/src/geo/ui/legends/bubble/placeholder-template.tpl new file mode 100644 index 0000000..3e2527f --- /dev/null +++ b/src/geo/ui/legends/bubble/placeholder-template.tpl @@ -0,0 +1,25 @@ +
+
+
+ + + + + + + + + + + + + + + +
+
+ + + + + diff --git a/src/geo/ui/legends/categories/legend-template.tpl b/src/geo/ui/legends/categories/legend-template.tpl new file mode 100644 index 0000000..a80e618 --- /dev/null +++ b/src/geo/ui/legends/categories/legend-template.tpl @@ -0,0 +1,14 @@ +<% if (categories && categories.length > 0) { %> +
    + <% for(var i in categories) { %> +
  • + <% if (categories[i].icon) { %> + + <% } else if (categories[i].color) { %> + + <% } %> +

    <%= categories[i].title %>

    +
  • + <% } %> +
+<% }%> diff --git a/src/geo/ui/legends/categories/legend-view.js b/src/geo/ui/legends/categories/legend-view.js new file mode 100644 index 0000000..10d410d --- /dev/null +++ b/src/geo/ui/legends/categories/legend-view.js @@ -0,0 +1,25 @@ +var LegendViewBase = require('../base/legend-view-base'); +var template = require('./legend-template.tpl'); + +var CategoryLegendView = LegendViewBase.extend({ + _getCompiledTemplate: function () { + return template({ + categories: this._getCategories() + }); + }, + + _getCategories: function () { + var categories = this.model.get('categories').slice(0); + var _default = this.model.get('default'); + if (_default) { + categories.push({ + title: 'Others', + icon: _default.icon, + color: _default.color + }); + } + return categories; + } +}); + +module.exports = CategoryLegendView; diff --git a/src/geo/ui/legends/categories/placeholder-template.tpl b/src/geo/ui/legends/categories/placeholder-template.tpl new file mode 100644 index 0000000..e40786d --- /dev/null +++ b/src/geo/ui/legends/categories/placeholder-template.tpl @@ -0,0 +1,14 @@ +
+
+
+ + + + + + + + + +
+
diff --git a/src/geo/ui/legends/choropleth/legend-template.tpl b/src/geo/ui/legends/choropleth/legend-template.tpl new file mode 100644 index 0000000..d3c5bc1 --- /dev/null +++ b/src/geo/ui/legends/choropleth/legend-template.tpl @@ -0,0 +1,16 @@ +
+ <% if (!hasCustomLabels) { %> +

<%- prefix %> <%= labels.left %> <%- suffix %>

+

<%- prefix %> <%= labels.right %> <%- suffix %>

+ <% } else { %> +

<%- prefix %> <%= labels.left %> <%- suffix %>

+

<%- prefix %> <%= labels.right %> <%- suffix %>

+ <% } %> +
+
+ <% if (!hasCustomLabels) { %> + + <%= formatter.formatNumber(avg) %> AVG + + <% } %> +
diff --git a/src/geo/ui/legends/choropleth/legend-view.js b/src/geo/ui/legends/choropleth/legend-view.js new file mode 100644 index 0000000..914fc04 --- /dev/null +++ b/src/geo/ui/legends/choropleth/legend-view.js @@ -0,0 +1,50 @@ +var LegendViewBase = require('../base/legend-view-base'); +var template = require('./legend-template.tpl'); +var formatter = require('../../../../util/formatter'); +var Sanitize = require('../../../../core/sanitize'); + +var ChoroplethLegendView = LegendViewBase.extend({ + _getCompiledTemplate: function () { + return template({ + colors: this.model.get('colors'), + avg: this.model.get('avg'), + hasCustomLabels: this._hasCustomLabels(), + avgPercentage: this._calculateAVGPercentage(), + prefix: this.model.get('prefix'), + suffix: this.model.get('suffix'), + labels: this._getLabels(), + formatter: formatter + }); + }, + + _hasCustomLabels: function () { + var leftLabel = this.model.get('leftLabel'); + var rightLabel = this.model.get('rightLabel'); + return ((leftLabel != null && leftLabel !== '') || (rightLabel != null && rightLabel !== '')); + }, + + _getLabels: function () { + var colors = this.model.get('colors'); + var leftLabel = this.model.get('leftLabel'); + var rightLabel = this.model.get('rightLabel'); + var o = {}; + o.left = Sanitize.html(formatter.formatNumber(colors[0].label)); + o.right = Sanitize.html(formatter.formatNumber(colors[colors.length - 1].label)); + if (leftLabel != null && leftLabel !== '') { + o.left = leftLabel; + } + + if (rightLabel != null && rightLabel !== '') { + o.right = rightLabel; + } + + return o; + }, + + // In order to work with negative values, we need to include the total range + _calculateAVGPercentage: function () { + return (this.model.get('avg') - this.model.get('min')) * 100 / (this.model.get('max') - this.model.get('min')); + } +}); + +module.exports = ChoroplethLegendView; diff --git a/src/geo/ui/legends/choropleth/placeholder-template.tpl b/src/geo/ui/legends/choropleth/placeholder-template.tpl new file mode 100644 index 0000000..9060fbd --- /dev/null +++ b/src/geo/ui/legends/choropleth/placeholder-template.tpl @@ -0,0 +1,13 @@ +
+
+
+ + + + + + + + +
+
diff --git a/src/geo/ui/legends/custom-choropleth/legend-template.tpl b/src/geo/ui/legends/custom-choropleth/legend-template.tpl new file mode 100644 index 0000000..f57e9cd --- /dev/null +++ b/src/geo/ui/legends/custom-choropleth/legend-template.tpl @@ -0,0 +1,10 @@ +
+ <% if (hasCustomLabels) { %> +

<%- prefix %> <%= leftLabel %> <%- suffix %>

+

<%- prefix %> <%= rightLabel %> <%- suffix %>

+ <% } else { %> +

<%- prefix %> <%- suffix %>

+

<%- prefix %> <%- suffix %>

+ <% } %> +
+
diff --git a/src/geo/ui/legends/custom-choropleth/legend-view.js b/src/geo/ui/legends/custom-choropleth/legend-view.js new file mode 100644 index 0000000..4fa63e0 --- /dev/null +++ b/src/geo/ui/legends/custom-choropleth/legend-view.js @@ -0,0 +1,23 @@ +var LegendViewBase = require('../base/legend-view-base'); +var template = require('./legend-template.tpl'); + +var ChoroplethLegendView = LegendViewBase.extend({ + _getCompiledTemplate: function () { + return template({ + colors: this.model.get('colors'), + prefix: this.model.get('prefix'), + suffix: this.model.get('suffix'), + hasCustomLabels: this._hasCustomLabels(), + leftLabel: this.model.get('leftLabel'), + rightLabel: this.model.get('rightLabel') + }); + }, + + _hasCustomLabels: function () { + var leftLabel = this.model.get('leftLabel'); + var rightLabel = this.model.get('rightLabel'); + return ((leftLabel != null && leftLabel !== '') || (rightLabel != null && rightLabel !== '')); + } +}); + +module.exports = ChoroplethLegendView; diff --git a/src/geo/ui/legends/custom/legend-template.tpl b/src/geo/ui/legends/custom/legend-template.tpl new file mode 100644 index 0000000..4c5af0f --- /dev/null +++ b/src/geo/ui/legends/custom/legend-template.tpl @@ -0,0 +1,14 @@ +<% if (items && items.length > 0) { %> +
    + <% for(var i in items) { %> +
  • + <% if (items[i].icon) { %> + + <% } else if (items[i].color) { %> + + <% } %> +

    <%= items[i].title %>

    +
  • + <% } %> +
+<% }%> diff --git a/src/geo/ui/legends/custom/legend-view.js b/src/geo/ui/legends/custom/legend-view.js new file mode 100644 index 0000000..6aa0a96 --- /dev/null +++ b/src/geo/ui/legends/custom/legend-view.js @@ -0,0 +1,24 @@ +var LegendViewBase = require('../base/legend-view-base'); +var template = require('./legend-template.tpl'); + +var CustomLegendView = LegendViewBase.extend({ + _getCompiledTemplate: function () { + var htmlTemplate = this.model.get('html'); + if (htmlTemplate === '') { + htmlTemplate = template({ + items: this.model.get('items') + }); + } else { + htmlTemplate = this._patchStylesForIE(htmlTemplate); + } + return htmlTemplate; + }, + + _patchStylesForIE: function (str) { + var find = /\bstyle=(['"])/g; + var replace = 'style="opacity:1; '; + return str.replace(find, replace); + } +}); + +module.exports = CustomLegendView; diff --git a/src/geo/ui/legends/layer-legends-template.tpl b/src/geo/ui/legends/layer-legends-template.tpl new file mode 100644 index 0000000..182e817 --- /dev/null +++ b/src/geo/ui/legends/layer-legends-template.tpl @@ -0,0 +1,17 @@ +

+ <% if (showLayerSelector) { %> + + <% if (isLayerVisible) { %> + + <% } else { %> + + <% } %> + + + <% } %> + <%- layerName %> +

+ +<% if (showLegends) { %> +
+<% } %> diff --git a/src/geo/ui/legends/layer-legends-view.js b/src/geo/ui/legends/layer-legends-view.js new file mode 100644 index 0000000..a6079d2 --- /dev/null +++ b/src/geo/ui/legends/layer-legends-view.js @@ -0,0 +1,123 @@ +var _ = require('underscore'); +var View = require('../../../core/view'); +var template = require('./layer-legends-template.tpl'); +var LegendViewFactory = require('./legend-view-factory'); + +var LayerLegendsView = View.extend({ + + className: 'CDB-LayerLegends js-layer-legends', + + events: { + 'click .js-toggle-layer': '_onToggleLayerCheckboxClicked' + }, + + initialize: function (options) { + this._legendViews = []; + + this.settingsModel = options.settingsModel; + + this.model.on('change:visible', this.render, this); + this.model.on('change:layer_name', this.render, this); + + this._getLegendModels().forEach(function (legendModel) { + legendModel.on('change:state', _.debounce(this.render, 150), this); + legendModel.on('change:visible', _.debounce(this.render, 150), this); + this.add_related_model(legendModel); + }, this); + + this.settingsModel.on('change', this.render, this); + this.add_related_model(this.settingsModel); + }, + + render: function () { + var showLegends = this._shouldLegendsBeVisible(); + var showLayerSelector = this._shouldLayerSelectorBeVisible(); + var shouldVisible = this._shouldLayerLegendsBeVisible(); + + if (shouldVisible) { + this.$el.html( + template({ + layerName: this.model.getName(), + isLayerVisible: this._isLayerVisible(), + showLegends: showLegends, + showLayerSelector: showLayerSelector + }) + ); + + this._renderLegends(); + } else { + this.$el.html(''); + } + + this.trigger('render'); + + return this; + }, + + isEmpty: function () { + return this.$el.html() === ''; + }, + + _shouldLegendsBeVisible: function () { + var showLegends = this.settingsModel.get('showLegends'); + return showLegends && this._isLayerVisible(); + }, + + _shouldLayerLegendsBeVisible: function () { + var showLegends = this.settingsModel.get('showLegends'); + var hasLegends = this.model.legends.hasAnyLegend(); + return this._shouldLayerSelectorBeVisible() || (this._isLayerVisible() && showLegends && hasLegends); + }, + + _shouldLayerSelectorBeVisible: function () { + var isLayerSelectorEnabled = this.settingsModel.get('layerSelectorEnabled'); + var showLayerSelector = this.settingsModel.get('showLayerSelector'); + var shouldVisible = showLayerSelector && isLayerSelectorEnabled; + + return shouldVisible; + }, + + _renderLegends: function () { + _.each(this._getLegendModels(), this._renderLegend, this); + }, + + _renderLegend: function (legendModel) { + var legendView = LegendViewFactory.createLegendView(legendModel); + this._legendViews.push(legendView); + this._legendsContainer().append(legendView.render().$el); + }, + + _legendsContainer: function () { + return this.$('.js-legends'); + }, + + _onToggleLayerCheckboxClicked: function (event) { + var isLayerEnabled = event.target.checked; + if (isLayerEnabled) { + this.model.show(); + } else { + this.model.hide(); + } + }, + + _getLegendViews: function () { + return this._legendViews || []; + }, + + _getLegendModels: function () { + return [ + this.model.legends.custom, + this.model.legends.choropleth, + this.model.legends.custom_choropleth, + this.model.legends.category, + this.model.legends.bubble, + this.model.legends.torque + ]; + }, + + _isLayerVisible: function () { + return this.model.isVisible(); + } +}); + +module.exports = LayerLegendsView; diff --git a/src/geo/ui/legends/legend-view-factory.js b/src/geo/ui/legends/legend-view-factory.js new file mode 100644 index 0000000..cf16082 --- /dev/null +++ b/src/geo/ui/legends/legend-view-factory.js @@ -0,0 +1,36 @@ +var BubbleLegendView = require('./bubble/legend-view'); +var CategoryLegendView = require('./categories/legend-view'); +var ChoroplethLegendView = require('./choropleth/legend-view'); +var CustomLegendView = require('./custom/legend-view'); +var CustomChoroplethLegendView = require('./custom-choropleth/legend-view'); + +var LEGEND_VIEW_CONSTRUCTORS = { + bubble: BubbleLegendView, + category: CategoryLegendView, + choropleth: ChoroplethLegendView, + custom_choropleth: CustomChoroplethLegendView, + custom: CustomLegendView, + torque: CustomLegendView +}; + +var PLACEHOLDER_TEMPLATES = { + bubble: require('./bubble/placeholder-template.tpl'), + category: require('./categories/placeholder-template.tpl'), + choropleth: require('./choropleth/placeholder-template.tpl') +}; + +module.exports = { + createLegendView: function (legendModel) { + var legendType = legendModel.get('type'); + var LegendViewClass = LEGEND_VIEW_CONSTRUCTORS[legendType]; + var placeholderTemplate = PLACEHOLDER_TEMPLATES[legendType]; + if (LegendViewClass) { + return new LegendViewClass({ + model: legendModel, + placeholderTemplate: placeholderTemplate + }); + } + + throw new Error('Legends of type "' + legendType + '" are not supported'); + } +}; diff --git a/src/geo/ui/legends/legends-view.js b/src/geo/ui/legends/legends-view.js new file mode 100644 index 0000000..9554512 --- /dev/null +++ b/src/geo/ui/legends/legends-view.js @@ -0,0 +1,156 @@ +var $ = require('jquery'); +var _ = require('underscore'); +var View = require('../../../core/view'); +var Ps = require('perfect-scrollbar'); +var template = require('./legends-view.tpl'); +var LayerLegendsView = require('./layer-legends-view'); + +var LegendsView = View.extend({ + + className: 'CDB-Legends-canvas', + + initialize: function (options) { + if (!options.layersCollection) throw new Error('layersCollection is required'); + if (!options.settingsModel) throw new Error('settingsModel is required'); + + this._layersCollection = options.layersCollection; + this.settingsModel = options.settingsModel; + this._isRendered = false; + + this._initBinds(); + + this._layerLegendsViews = []; + }, + + _initBinds: function () { + this.listenTo(this._layersCollection, 'add remove layerMoved', this._onLayersChanged); + this.listenTo(this.settingsModel, 'change', this._onSettingsModelChanged); + }, + + render: function () { + this.$el.html(template()); + var layerModelsWithLegends = this._layersCollection.getLayersWithLegends(); + _.each(layerModelsWithLegends.reverse(), this._renderLayerLegends, this); + this._isRendered = true; + this._renderScroll(); + this._renderShadows(); + this._bindScroll(); + this._showOrHide(); + return this; + }, + + _renderScroll: function () { + Ps.initialize(this._container(), { + wheelSpeed: 1, + wheelPropagation: false, + swipePropagation: true, + stopPropagationOnClick: false, + minScrollbarLength: 20, + suppressScrollX: true + }); + }, + + _renderShadows: function () { + this.$shadowTop = $('
').addClass('CDB-Legends-canvasShadow CDB-Legends-canvasShadow--top'); + this.$shadowBottom = $('
').addClass('CDB-Legends-canvasShadow CDB-Legends-canvasShadow--bottom'); + this.$el.append(this.$shadowTop); + this.$el.append(this.$shadowBottom); + _.defer(function () { + this._showOrHideShadows(); + }.bind(this)); + }, + + _bindScroll: function () { + this.$(this._container()) + .on('ps-y-reach-start', _.bind(this._onScrollTop, this)) + .on('ps-y-reach-end', _.bind(this._onScrollBottom, this)) + .on('ps-scroll-y', _.bind(this._onScroll, this)); + }, + + _onScrollTop: function () { + this.$shadowTop.removeClass('is-visible'); + }, + + _onScroll: function () { + this._showOrHideShadows(); + }, + + _showOrHideShadows: function () { + var $el = $(this._container()); + if ($el.length) { + var currentPos = $el.scrollTop(); + var max = $el.get(0).scrollHeight; + var height = $el.outerHeight(); + var maxPos = max - height; + + this.$shadowTop.toggleClass('is-visible', currentPos > 0); + this.$shadowBottom.toggleClass('is-visible', currentPos < maxPos); + } + }, + + _onScrollBottom: function () { + this.$shadowBottom.removeClass('is-visible'); + }, + + _container: function () { + return this.el.querySelector('.js-container'); + }, + + _renderLayerLegends: function (layerModel) { + var layerLegendsView = new LayerLegendsView({ + model: layerModel, + settingsModel: this.settingsModel + }); + + this._layerLegendsViews.push(layerLegendsView); + layerLegendsView.on('render', this._showOrHide, this); + this.addView(layerLegendsView); + + this.$(this._container()).append(layerLegendsView.render().$el); + }, + + _onLayersChanged: function (layerModel) { + // If view has already been rendered and a layer is added / removed + if (this._isRendered && this._hasLegends(layerModel)) { + this._clear(); + this.render(); + } + }, + + _showOrHide: function () { + this.$el.toggle(this._isAnyLayerLegendViewVisible()); + }, + + _isAnyLayerLegendViewVisible: function () { + return _.any(this._layerLegendsViews, function (layerLegendView) { + return !layerLegendView.isEmpty(); + }); + }, + + _hasLegends: function (layerModel) { + return !!layerModel.legends; + }, + + _clear: function () { + _.each(this._layerLegendsViews, function (layerLegendView) { + layerLegendView.clean(); + }); + this._layerLegendsViews = []; + this.$el.html(''); + }, + + show: function () { + this.$el.show(); + }, + + hide: function () { + this.$el.hide(); + }, + + clean: function () { + View.prototype.clean.call(this); + this._clear(); + } +}); + +module.exports = LegendsView; diff --git a/src/geo/ui/legends/legends-view.tpl b/src/geo/ui/legends/legends-view.tpl new file mode 100644 index 0000000..92c2d34 --- /dev/null +++ b/src/geo/ui/legends/legends-view.tpl @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/src/geo/ui/limits/limits-view.js b/src/geo/ui/limits/limits-view.js new file mode 100644 index 0000000..0f1bd5e --- /dev/null +++ b/src/geo/ui/limits/limits-view.js @@ -0,0 +1,22 @@ +var View = require('../tiles/tiles-view.js'); +var template = require('../tiles/tiles-template.tpl'); +var Sanitize = require('../../../core/sanitize'); + +/** + * Limits overlay + * + */ + +module.exports = View.extend({ + className: 'CDB-Limits', + + render: function () { + this.$el.html( + template({ + limits: Sanitize.html('Some tiles might not be rendering correctly.
Learn More') + }) + ); + + return this; + } +}); diff --git a/src/geo/ui/logo-view.js b/src/geo/ui/logo-view.js new file mode 100644 index 0000000..faa84a7 --- /dev/null +++ b/src/geo/ui/logo-view.js @@ -0,0 +1,15 @@ +var View = require('../../core/view'); +var template = require('./logo.tpl'); +var URL = 'https://carto.com'; + +module.exports = View.extend({ + className: 'CDB-Logo', + tagName: 'a', + + render: function () { + this.$el.html(template()); + this.$el.attr('href', URL); + this.$el.attr('target', '_blank'); + return this; + } +}); diff --git a/src/geo/ui/logo.tpl b/src/geo/ui/logo.tpl new file mode 100644 index 0000000..461b0fa --- /dev/null +++ b/src/geo/ui/logo.tpl @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/geo/ui/overlays-container.tpl b/src/geo/ui/overlays-container.tpl new file mode 100644 index 0000000..e86f578 --- /dev/null +++ b/src/geo/ui/overlays-container.tpl @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/src/geo/ui/overlays-view.js b/src/geo/ui/overlays-view.js new file mode 100644 index 0000000..d5d5652 --- /dev/null +++ b/src/geo/ui/overlays-view.js @@ -0,0 +1,171 @@ +var _ = require('underscore'); +var View = require('../../core/view'); +var OverlaysFactory = require('../../vis/overlays-factory'); +var overlayContainerTemplate = require('./overlays-container.tpl'); +var C = require('../../constants'); +var Engine = require('../../engine'); + +var CONTAINED_OVERLAYS = ['attribution', 'fullscreen', 'tiles', 'limits', 'logo', 'search', 'zoom']; + +var OverlaysView = View.extend({ + initialize: function (opts) { + if (!opts.overlaysCollection) throw new Error('overlaysCollection is required'); + if (!opts.engine) throw new Error('engine is required'); + if (!opts.visView) throw new Error('visView is required'); + if (!opts.mapModel) throw new Error('mapModel is required'); + if (!opts.mapView) throw new Error('mapView is required'); + + this._overlaysCollection = opts.overlaysCollection; + this._engine = opts.engine; + this._visView = opts.visView; + this._mapModel = opts.mapModel; + this._mapView = opts.mapView; + + this._overlayViews = []; + this._overlaysFactory = new OverlaysFactory({ + mapModel: this._mapModel, + mapView: this._mapView, + visView: this._visView + }); + + this._initBinds(); + + this.$el.append(overlayContainerTemplate()); + }, + + render: function () { + this._clearOverlays(); + this._renderOverlays(); + return this; + }, + + _initBinds: function () { + this._engine.on(Engine.Events.RELOAD_STARTED, this._showLoaderOverlay, this); + this._engine.on(Engine.Events.RELOAD_ERROR, this._hideLoaderOverlay, this); + this._engine.on(Engine.Events.RELOAD_SUCCESS, this._hideLoaderOverlay, this); + + this.listenTo(this._overlaysCollection, 'add remove change', this.render, this); + this.listenTo(this._mapModel, 'error:limit', this._addLimitsOverlay, this); + this.listenTo(this._mapModel, 'error:tile', this._addTilesOverlay, this); + }, + + _clearOverlays: function () { + while (this._overlayViews.length !== 0) { + this._overlayViews.pop().clean(); + } + }, + + _renderOverlays: function () { + var overlays = this._overlaysCollection.toJSON(); + + // Sort the overlays by its internal order + overlays = _.sortBy(overlays, function (overlay) { + return overlay.order === null ? Number.MAX_VALUE : overlay.order; + }); + + overlays.forEach(function (data) { + this._renderOverlay(data); + }.bind(this)); + }, + + _renderOverlay: function (overlay) { + var overlayView = this._createOverlayView(overlay); + if (!overlayView) return; + + overlayView.render(); + + this._isGlobalOverlay(overlay) + ? this.$el.append(overlayView.el) + : this._overlayContainer().append(overlayView.el); + + this.addView(overlayView); + this._overlayViews.push(overlayView); + }, + + _createOverlayView: function (overlay) { + return this._overlaysFactory.create(overlay.type, overlay, { + visView: this._visView, + map: this._mapModel + }); + }, + + _isGlobalOverlay: function (overlay) { + return CONTAINED_OVERLAYS.indexOf(overlay.type) === -1; + }, + + _overlayContainer: function () { + return this.$('.CDB-OverlayContainer'); + }, + + _showLoaderOverlay: function () { + var loaderOverlay = this._getLoaderOverlay(); + if (!loaderOverlay) return; + loaderOverlay.show(); + }, + + _hideLoaderOverlay: function () { + var loaderOverlay = this._getLoaderOverlay(); + if (!loaderOverlay) return; + loaderOverlay.hide(); + }, + + _getLoaderOverlay: function () { + return this._getOverlayViewByType('loader'); + }, + + _getOverlayViewByType: function (type) { + return _.find(this._overlayViews, function (overlayView) { + return overlayView.type === type; + }); + }, + + _areLimitsErrorsEnabled: function () { + return this._visView.model.get('showLimitErrors'); + }, + + _addLimitsOverlay: function () { + if (!this._areLimitsErrorsEnabled()) return; + this._removeTilesOverlay(); + + var limitsOverlay = this._getOverlayViewByType(C.OVERLAY_TYPES.LIMITS); + + limitsOverlay || this._overlaysCollection.add({ + type: C.OVERLAY_TYPES.LIMITS + }); + }, + + _hasLimitsOverlay: function () { + return !!this._getOverlayViewByType(C.OVERLAY_TYPES.LIMITS); + }, + + _addTilesOverlay: function () { + if (this._hasLimitsOverlay()) return; + + var tilesOverlay = this._getOverlayViewByType(C.OVERLAY_TYPES.TILES); + + tilesOverlay || this._overlaysCollection.add({ + type: C.OVERLAY_TYPES.TILES + }); + }, + + _removeLimitsOverlay: function () { + var overlay = this._overlaysCollection.findWhere({ + type: C.OVERLAY_TYPES.LIMITS + }); + this._overlaysCollection.remove(overlay); + }, + + _removeTilesOverlay: function () { + var overlay = this._overlaysCollection.findWhere({ + type: C.OVERLAY_TYPES.TILES + }); + this._overlaysCollection.remove(overlay); + }, + + clean: function () { + View.prototype.clean.apply(this); + this._clearOverlays(); + } +}); + +module.exports = OverlaysView; diff --git a/src/geo/ui/search/search.js b/src/geo/ui/search/search.js new file mode 100644 index 0000000..823192b --- /dev/null +++ b/src/geo/ui/search/search.js @@ -0,0 +1,237 @@ +var _ = require('underscore'); +var View = require('../../../core/view'); +var mapboxGeocoder = require('../../../geo/geocoder/mapbox-geocoder'); +var tomtomGeocoder = require('../../../geo/geocoder/tomtom-geocoder'); +var InfowindowModel = require('../../../geo/ui/infowindow-model'); +var Infowindow = require('../../../geo/ui/infowindow-view'); +var Point = require('../../../geo/geometry-models/point.js'); +var template = require('./search_template.tpl'); +var infowindowTemplate = require('./search_infowindow_template.tpl'); + +var DEFAULT_GEOCODER = 'tomtom'; + +var GEOCODERS = { + 'tomtom': tomtomGeocoder, + 'mapbox': mapboxGeocoder +}; + +var GEOCODERS_WINDOW_API_KEYS = { + 'tomtom': 'tomtomApiKey', + 'mapbox': 'mapboxApiKey' +}; + +/** + * UI component to place the map in the + * location found by the geocoder. + */ + +var Search = View.extend({ + className: 'CDB-Search CDB-Overlay', + + _ZOOM_BY_CATEGORY: { + 'address': 18, + 'building': 18, + 'venue': 18, + 'postal-area': 15, + 'neighbourhood': 15, + 'locality': 12, + 'localadmin': 11, + 'region': 8, + 'county': 8, + 'country': 5, + 'default': 12 + }, + + events: { + 'click .js-toggle': '_onToggleClick', + 'submit .js-form': '_onSubmit', + 'click': '_stopPropagation', + 'dblclick': '_stopPropagation', + 'mousedown': '_stopPropagation' + }, + + options: { + searchPin: true, + infowindowWidth: 186, + infowindowOffset: [30, 30], + iconUrl: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAfCAYAAADXwvzvAAACuklEQVR4Ae3PQ+AsNxzA8e8vo/Xus237vVN9qW3b7qW2bdu2caxt29bu/meSmaTpqW63Pfc7wemTZPh9K/Xv3zhzxIgVrho0aMsLGo2N9o+iuYDwV02E5NJpM7d5fMGC515dMP/7l6dNMc+OGJY9Uq99cVMc33I4LOJXCQBQuXPBglNnDRm0Xa1RAWewP3yL/vJLul99Q/pNm0/b+qsnbLHngXAVgAI4b9KkXWc1m9vV58ykst56lKdMptyokdTKRJUIV1MMTGTgbOTknWABgFo2SSbOjuN9wlgIBrSIJ0yiVG9QUgGxUigRRAlpCQYrBs+A/QClliuXV6ppPVibDPPqi5irL8G+/QY2S3FZhityrLNYBWkAI2G5WTA2nGTthKDTJfP/FH1sCb76nNBa7I8/knba6Eyj8wJjLbk4qlCdAFNClWXKiiL72kGRUkSRhwUuTUm7XTqZ3z3KnMM7QhAFUfiKMZ9OQci+ydFFH32BIsDh8hxjDF2T0y0KtHHUczCg34P3wgesfWhZozstW1R/cJpuohA8dI7cWrSfxqM4gwEOnoJnn4HXBVDHwHnriNr2W3G0I8FEkKufMbjcIw1DC+iCuRw2OBduEYAKDD8drlkGlk6BHwAtIEDioD/QBnsnnHAI7A9YAAAGenwEnPuAd8+DewHcS+CeB3szvL0b7ADE/FWzYf5BCxa9dMvqa7oLll7WbTlsxKkDYRi9dPqhRz743L0PuKtOPMXtutHmm/InKf5Y6Co15Upl8qSCqVajXiEeUTRb6GqNIojoGaLEDwEA6B0KIKL8lH8JBeS/3AgK73qAPfc/tCLiAACUCmyvsJHnphwEAYFStNs/NoHgn2ATWPmlF54b/9GHH/Khn88/+9SywJx/+q0SsKTZbB45d/6CO0aNHnutv3kbYDQg9JAAIRDwF/0EjlkjUi3fkAMAAAAASUVORK5CYII=', + iconAnchor: [7, 31] + }, + + initialize: function () { + this.map = this.model; + this.mapView = this.options.mapView; + this.template = this.options.template || template; + + this._initializeGeocoder(); + }, + + _initializeGeocoder: function () { + const injectedGeocoderConfig = this._getGeocodingInfoFromConfig(); + + this.geocoderService = this.options.geocoderService || injectedGeocoderConfig.provider || DEFAULT_GEOCODER; + this.geocoder = GEOCODERS[this.geocoderService]; + + const windowApiKey = GEOCODERS_WINDOW_API_KEYS[this.geocoderService]; + this.token = this.options.token || injectedGeocoderConfig.token || window[windowApiKey]; + }, + + render: function () { + this.$el.html(this.template(this.options)); + return this; + }, + + _stopPropagation: function (ev) { + if (ev) { + ev.stopPropagation(); + } + }, + + _onSubmit: function (ev) { + ev.preventDefault(); + var address = this.$('.js-textInput').val(); + + if (!address) { + return; + } + // Remove previous pin without any timeout (0 represents the timeout for + // the animation) + this._destroySearchPin(0); + // TODO: we a Better way to pass api keys + return this.geocoder.geocode(address, this.token).then(this._onResult.bind(this)); + }, + + _onResult: function (places) { + var address = this.$('.js-textInput').val(); + + if (places && places.length > 0) { + var location = places[0]; + var zoom = this._getZoomByCategory(location.type); + this.model.setView(location.center, zoom, { silent: false }); + + if (this.options.searchPin) { + this._createSearchPin(location.center, address); + } + } + }, + + _onToggleClick: function () { + this.$('.CDB-Search-inner').toggleClass('is-active'); + this.$('.js-textInput').focus(); + }, + + // Getting zoom for each type of location + _getZoomByCategory: function (type) { + if (type && this._ZOOM_BY_CATEGORY[type]) { + return this._ZOOM_BY_CATEGORY[type]; + } + return this._ZOOM_BY_CATEGORY['default']; + }, + + _createSearchPin: function (position, address) { + this._destroySearchPin(); + this._createPin(position, address); + this._createInfowindow(position, address); + this._bindEvents(); + }, + + _destroySearchPin: function (timeout) { + this._unbindEvents(); + this._destroyPin(); + this._destroyInfowindow(timeout); + }, + + _createInfowindow: function (position, address) { + var infowindowModel = new InfowindowModel({ + template: infowindowTemplate, + latlng: position, + width: this.options.infowindowWidth, + offset: this.options.infowindowOffset, + content: { + fields: [{ + title: 'address', + value: address + }] + } + }); + + this._searchInfowindow = new Infowindow({ + model: infowindowModel, + mapView: this.mapView + }); + + this.mapView.$el.append(this._searchInfowindow.el); + infowindowModel.set('visibility', true); + }, + + _destroyInfowindow: function (timeout) { + if (this._searchInfowindow) { + timeout = !_.isUndefined(timeout) && _.isNumber(timeout) ? timeout : 500; + // Hide it and then destroy it (when animation ends) + this._searchInfowindow.hide(true); + var self = this; + setTimeout(function () { + if (self._searchInfowindow) { + self._searchInfowindow.clean(); + delete self._searchInfowindow; + } + }, timeout); + } + }, + + _createPin: function (position, address) { + this._searchPin = new Point({ + latlng: [position[0], position[1]], + iconUrl: this.options.iconUrl, + iconAnchor: this.options.iconAnchor + }); + + this.map.addGeometry(this._searchPin); + }, + + _toggleSearchInfowindow: function () { + var infowindowVisibility = this._searchInfowindow.model.get('visibility'); + this._searchInfowindow.model.set('visibility', !infowindowVisibility); + }, + + _destroyPin: function () { + if (this._searchPin) { + this.map.removeGeometry(this._searchPin); + delete this._searchPin; + } + }, + + _bindEvents: function () { + this._searchPin && this._searchPin.bind('click', this._toggleSearchInfowindow, this); + this.mapView.bind('click', this._destroySearchPin, this); + }, + + _unbindEvents: function () { + this._searchPin && this._searchPin.unbind('click', this._toggleSearchInfowindow, this); + this.mapView.unbind('click', this._destroySearchPin, this); + }, + + _getGeocodingInfoFromConfig () { + if (!window.geocoderConfiguration) { + return {}; + } + + const provider = window.geocoderConfiguration.provider; + let token; + + if (provider) { + token = window.geocoderConfiguration[provider] && + window.geocoderConfiguration[provider].search_bar_api_key; + } + + return { provider, token }; + }, + + clean: function () { + this._unbindEvents(); + this._destroySearchPin(); + View.prototype.clean.call(this); + } +}); + +module.exports = Search; diff --git a/src/geo/ui/search/search_infowindow_template.tpl b/src/geo/ui/search/search_infowindow_template.tpl new file mode 100644 index 0000000..55592d6 --- /dev/null +++ b/src/geo/ui/search/search_infowindow_template.tpl @@ -0,0 +1,15 @@ +
+
+
+
+
+

<%- address %>

+
+
+
+
+
+
+
+
+
\ No newline at end of file diff --git a/src/geo/ui/search/search_template.tpl b/src/geo/ui/search/search_template.tpl new file mode 100644 index 0000000..34b799d --- /dev/null +++ b/src/geo/ui/search/search_template.tpl @@ -0,0 +1,6 @@ +
+ + +
diff --git a/src/geo/ui/tiles-loader.js b/src/geo/ui/tiles-loader.js new file mode 100644 index 0000000..532b176 --- /dev/null +++ b/src/geo/ui/tiles-loader.js @@ -0,0 +1,49 @@ +var View = require('../../core/view'); + +/** + * Show or hide tiles loader + * + * Usage: + * + * var tiles_loader = new TilesLoader(); + * mapWrapper.$el.append(tiles_loader.render().$el); + */ +var TilesLoader = View.extend({ + className: 'CDB-Loader', + + options: { + animationSpeed: 500 + }, + + initialize: function () { + this.isVisible = 0; + }, + + render: function () { + return this; + }, + + show: function (ev) { + if (this.isVisible) { + return; + } + this.$el.addClass('is-visible'); + this.isVisible++; + }, + + hide: function (ev) { + this.isVisible--; + if (this.isVisible > 0) { + return; + } + this.isVisible = 0; + this.$el.removeClass('is-visible'); + }, + + visible: function () { + return this.isVisible > 0; + } + +}); + +module.exports = TilesLoader; diff --git a/src/geo/ui/tiles/tiles-template.tpl b/src/geo/ui/tiles/tiles-template.tpl new file mode 100644 index 0000000..104db54 --- /dev/null +++ b/src/geo/ui/tiles/tiles-template.tpl @@ -0,0 +1,18 @@ +
+ +

+ <%= limits %> +

+
diff --git a/src/geo/ui/tiles/tiles-view.js b/src/geo/ui/tiles/tiles-view.js new file mode 100644 index 0000000..bbb38a0 --- /dev/null +++ b/src/geo/ui/tiles/tiles-view.js @@ -0,0 +1,92 @@ +var $ = require('jquery'); +var View = require('../../../core/view'); +var Model = require('../../../core/model'); +var template = require('./tiles-template.tpl'); +var Sanitize = require('../../../core/sanitize'); + +var ESC_KEY_CODE = 27; + +/** + * Limits overlay + * + */ + +module.exports = View.extend({ + className: 'CDB-Limits CDB-Limits--short', + + events: { + 'click .js-button': '_toggleLimits', + 'dblclick': 'killEvent' + }, + + initialize: function () { + this.model = new Model({ + visible: false + }); + this.map = this.options.map; + + this._onDocumentClick = this._onDocumentClick.bind(this); + this._onDocumentKeyDown = this._onDocumentKeyDown.bind(this); + + this._initBinds(); + }, + + render: function () { + this.$el.html( + template({ + limits: Sanitize.html('Some tiles might not be rendering correctly.') + }) + ); + + return this; + }, + + _initBinds: function () { + this.model.on('change:visible', this._onVisibleChanged, this); + }, + + _enableDocumentBinds: function () { + $(document).bind('keydown', this._onDocumentKeyDown); + $(document).bind('click', this._onDocumentClick); + }, + + _disableDocumentBinds: function () { + $(document).unbind('keydown', this._onDocumentKeyDown); + $(document).unbind('click', this._onDocumentClick); + }, + + _onDocumentKeyDown: function (event) { + if (event && event.keyCode === ESC_KEY_CODE) { + this._toggleLimits(); + } + }, + + _showLimits: function () { + this.$el.addClass('is-active'); + this._enableDocumentBinds(); + }, + + _hideLimits: function () { + this.$el.removeClass('is-active'); + this._disableDocumentBinds(); + }, + + _toggleLimits: function () { + this.model.set('visible', !this.model.get('visible')); + }, + + _onDocumentClick: function (event) { + if (!$(event.target).closest(this.el).length) { + this._toggleLimits(); + } + }, + + _onVisibleChanged: function (_model, isVisible) { + isVisible ? this._showLimits() : this._hideLimits(); + }, + + clean: function () { + this._disableDocumentBinds(); + View.prototype.clean.call(this); + } +}); diff --git a/src/geo/ui/tooltip-model.js b/src/geo/ui/tooltip-model.js new file mode 100644 index 0000000..170e634 --- /dev/null +++ b/src/geo/ui/tooltip-model.js @@ -0,0 +1,81 @@ +var _ = require('underscore'); +var Model = require('../../core/model'); +var InfowindowModel = require('./infowindow-model'); + +var TooltipModel = Model.extend({ + defaults: { + template: '

{{text}}

', + fields: [], + alternative_names: {}, + placement: 'bottom|right', + position: { x: 0, y: 0 }, + offset: [0, 0], + blacklisted_columns: ['cartodb_id'] + }, + + updateContent: function (attributes) { + var data = attributes; + var nonValidKeys = ['fields', 'content']; + + if (this.get('blacklisted_columns')) { + nonValidKeys = nonValidKeys.concat(this.get('blacklisted_columns')); + } + + var c = InfowindowModel.contentForFields(data, this.get('fields'), { + show_empty_fields: false + }); + + // Remove fields and content from data + // and make them visible for custom templates + data.content = _.omit(data, nonValidKeys); + + // loop through content values + data.fields = c.fields; + + // alternamte names + var names = this.get('alternative_names'); + if (names) { + data.fields.forEach(function (field) { + field.title = names[field.title] || field.title; + }); + } + + this.set('content', data); + }, + + setTooltipTemplate: function (tooltipTemplate) { + this.set({ + template: tooltipTemplate.get('template'), + fields: tooltipTemplate.fields.toJSON(), + alternative_names: tooltipTemplate.get('alternative_names') + }); + }, + + setPosition: function (position) { + this.set('position', position); + }, + + show: function () { + this.set('visible', true); + }, + + hide: function () { + this.set('visible', false); + }, + + isVisible: function () { + return !!this.get('visible'); + }, + + getVerticalOffset: function () { + var offset = this.get('offset'); + return (offset && offset[1]) || 0; + }, + + getHorizontalOffset: function () { + var offset = this.get('offset'); + return (offset && offset[0]) || 0; + } +}); + +module.exports = TooltipModel; diff --git a/src/geo/ui/tooltip-view.js b/src/geo/ui/tooltip-view.js new file mode 100644 index 0000000..9eff8c6 --- /dev/null +++ b/src/geo/ui/tooltip-view.js @@ -0,0 +1,149 @@ +var _ = require('underscore'); +var sanitize = require('../../core/sanitize'); +var Template = require('../../core/template'); +var View = require('../../core/view'); + +var FADE_IN_DURATION = 200; +var FADE_OUT_DURATION = 100; +var FADE_TIMEOUT = 50; + +var TooltipView = View.extend({ + defaultTemplate: '

{{text}}

', + className: 'CDB-Tooltip-wrapper', + + initialize: function (options) { + if (!options.mapView) { + throw new Error('mapView should be present'); + } + + this._mapView = options.mapView; + + this.showing = false; + this.showhideTimeout = null; + + this.model.bind('change:visible', this._showOrHide, this); + this.model.bind('change:position', this._updatePosition, this); + this.model.bind('change:placement', this._updatePosition, this); + this.model.bind('change:content change:alternative_names', this.render, this); + }, + + template: function (data) { + var compiledTemplate = Template.compile(this.model.get('template'), 'mustache'); + return compiledTemplate(data); + }, + + render: function () { + var content = this.model.get('content'); + var sanitizedOutput = sanitize.html(this.template(content)); + this.$el.html(sanitizedOutput); + this._updatePosition(); + return this; + }, + + _showOrHide: function () { + if (this.model.isVisible()) { + this._show(); + } else { + this._hide(); + } + }, + + _hide: function () { + var self = this; + var fadeOut = function () { + self.$el.fadeOut(FADE_OUT_DURATION); + }; + + clearTimeout(this.showhideTimeout); + this.showhideTimeout = setTimeout(fadeOut, FADE_TIMEOUT); + }, + + _show: function () { + this.render(); + + var self = this; + var fadeIn = function () { + self.$el.fadeIn(FADE_IN_DURATION); + }; + + clearTimeout(this.showhideTimeout); + this.showhideTimeout = setTimeout(fadeIn, FADE_TIMEOUT); + }, + + _updatePosition: function () { + var position = this.model.get('position'); + var placement = this.model.get('placement'); + var height = this.$el.innerHeight(); + var width = this.$el.innerWidth(); + var mapViewSize = this._mapView.getSize(); + var top = 0; + var left = 0; + var modifierClass = 'CDB-Tooltip-wrapper--'; + + // Remove any position modifier + this._removePositionModifiers(); + + // Vertically + if (placement.indexOf('top') !== -1) { + top = position.y - height; + } else if (placement.indexOf('middle') !== -1) { + top = position.y - (height / 2); + } else { // bottom + top = position.y; + } + + // Fix vertical overflow + if (top < 0) { + top = position.y; + modifierClass += 'top'; + } else if (top + height > mapViewSize.y) { + top = position.y - height; + modifierClass += 'bottom'; + } else { + modifierClass += 'top'; + } + + // Horizontally + if (placement.indexOf('left') !== -1) { + left = position.x - width; + } else if (placement.indexOf('center') !== -1) { + left = position.x - (width / 2); + } else { // right + left = position.x; + } + + // Fix horizontal overflow + if (left < 0) { + left = position.x; + modifierClass += 'Left'; + } else if (left + width > mapViewSize.x) { + left = position.x - width; + modifierClass += 'Right'; + } else { + modifierClass += 'Left'; + } + + // Add offsets + top += this.model.getVerticalOffset(); + left += this.model.getHorizontalOffset(); + + this.$el.css({ + top: top, + left: left + }).addClass(modifierClass); + }, + + _removePositionModifiers: function () { + var positions = [ 'topLeft', 'topRight', 'bottomRight', 'bottomLeft' ]; + var positionModifiers = _.map(positions, function (className) { + return this._modifierClassName(className); + }, this); + this.$el.removeClass(positionModifiers.join(' ')); + }, + + _modifierClassName: function (className) { + return this.className + '--' + className; + } +}); + +module.exports = TooltipView; diff --git a/src/geo/ui/zoom/zoom-template.tpl b/src/geo/ui/zoom/zoom-template.tpl new file mode 100644 index 0000000..27ae833 --- /dev/null +++ b/src/geo/ui/zoom/zoom-template.tpl @@ -0,0 +1,5 @@ +
+ +
-
+ +
diff --git a/src/geo/ui/zoom/zoom-view.js b/src/geo/ui/zoom/zoom-view.js new file mode 100644 index 0000000..b8a1efd --- /dev/null +++ b/src/geo/ui/zoom/zoom-view.js @@ -0,0 +1,63 @@ +var View = require('../../../core/view'); +var template = require('./zoom-template.tpl'); + +/** + * View to control the zoom of the map. + * + * Usage: + * + * var zoomControl = new Zoom({ model: map, template: "" }); + * mapWrapper.$el.append(zoomControl.render().$el); + * + */ + +module.exports = View.extend({ + className: 'CDB-Zoom', + + events: { + 'click .js-zoomIn': '_onZoomIn', + 'click .js-zoomOut': '_onZoomOut', + 'dblclick': 'killEvent' + }, + + options: { + timeout: 0, + msg: '' + }, + + initialize: function () { + this.map = this.model; + this.template = this.options.template || template; + this.map.bind('change:zoom change:minZoom change:maxZoom', this._checkZoom, this); + }, + + render: function () { + this.$el.html(this.template(this.options)); + this._checkZoom(); + return this; + }, + + _checkZoom: function () { + var zoom = this.map.get('zoom'); + this.$('.js-zoomIn')[ zoom < this.map.get('maxZoom') ? 'removeClass' : 'addClass' ]('is-disabled'); + this.$('.js-zoomOut')[ zoom > this.map.get('minZoom') ? 'removeClass' : 'addClass' ]('is-disabled'); + this.$('.js-zoomInfo').text(zoom); + }, + + _onZoomIn: function (ev) { + this.killEvent(ev); + + if (this.map.get('maxZoom') > this.map.getZoom()) { + this.map.setZoom(this.map.getZoom() + 1); + } + }, + + _onZoomOut: function (ev) { + this.killEvent(ev); + + if (this.map.get('minZoom') < this.map.getZoom()) { + this.map.setZoom(this.map.getZoom() - 1); + } + } + +}); diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..37956cf --- /dev/null +++ b/src/index.js @@ -0,0 +1,11 @@ +// Add polyfill for `fetch` +require('whatwg-fetch'); +// Add polyfill for `Promise` +var Promise = require('promise-polyfill'); +if (!window.Promise) { + window.Promise = Promise; +} + +var cdb = require('./cartodb.js'); + +module.exports = cdb; diff --git a/src/ui/common/fullscreen/fullscreen-template.tpl b/src/ui/common/fullscreen/fullscreen-template.tpl new file mode 100644 index 0000000..b9b8d8c --- /dev/null +++ b/src/ui/common/fullscreen/fullscreen-template.tpl @@ -0,0 +1,6 @@ + diff --git a/src/ui/common/fullscreen/fullscreen-view.js b/src/ui/common/fullscreen/fullscreen-view.js new file mode 100644 index 0000000..83f165d --- /dev/null +++ b/src/ui/common/fullscreen/fullscreen-view.js @@ -0,0 +1,118 @@ +var $ = require('jquery'); +var _ = require('underscore'); +var log = require('cdb.log'); +var View = require('../../../core/view'); +var template = require('./fullscreen-template.tpl'); + +/** + * FullScreen widget: + * + * var widget = new FullScreen({ + * doc: ".container", // optional; if not specified, we do the fullscreen of the whole window + * template: this.getTemplate("table/views/fullscreen") + * }); + * + */ +var FullScreen = View.extend({ + tagName: 'div', + className: 'CDB-Fullscreen', + + events: { + 'click': '_toggleFullScreen' + }, + + initialize: function () { + _.bindAll(this, 'render'); + this.template = this.options.template || template; + this._addWheelEvent(); + }, + + render: function () { + var options = _.extend( + this.options, + { + mapUrl: window.location.href || '' + } + ); + this.$el.html(this.template(options)); + + if (!this._canFullScreenBeEnabled()) { + this.undelegateEvents(); + log.info('FullScreen API is deprecated on insecure origins. See https://goo.gl/rStTGz for more details.'); + } + + return this; + }, + + _addWheelEvent: function () { + var self = this; + var mapView = this.options.mapView; + + $(document).on('webkitfullscreenchange mozfullscreenchange fullscreenchange', function () { + if (!document.fullscreenElement && !document.webkitFullscreenElement && !document.mozFullScreenElement && !document.msFullscreenElement) { + if (self.options.allowWheelOnFullscreen) { + mapView.options.map.set('scrollwheel', false); + } + } + mapView.invalidateSize(); + }); + }, + + _toggleFullScreen: function (ev) { + if (ev) { + this.killEvent(ev); + } + + var doc = window.document; + var docEl = doc.documentElement; + + if (this.options.doc) { // we use a custom element + docEl = this.options.doc; + } + + var requestFullScreen = docEl.requestFullscreen || docEl.mozRequestFullScreen || docEl.webkitRequestFullScreen || docEl.msRequestFullscreen; + var cancelFullScreen = doc.exitFullscreen || doc.mozCancelFullScreen || doc.webkitExitFullscreen || doc.msExitFullscreen; + var mapView = this.options.mapView; + + if (!doc.fullscreenElement && !doc.mozFullScreenElement && !doc.webkitFullscreenElement && !doc.msFullscreenElement) { + if (docEl.webkitRequestFullScreen) { + // Cartodb.js #361 :: Full screen button not working on Safari 8.0.3 #361 + // Safari has a bug that fullScreen doestn't work with Element.ALLOW_KEYBOARD_INPUT); + // Reference: Ehttp://stackoverflow.com/questions/8427413/webkitrequestfullscreen-fails-when-passing-element-allow-keyboard-input-in-safar + requestFullScreen.call(docEl, undefined); + } else { + // CartoDB.js #412 :: Fullscreen button is throwing errors + // Nowadays (2015/03/25), fullscreen is not supported in iOS Safari. Reference: http://caniuse.com/#feat=fullscreen + if (requestFullScreen) { + requestFullScreen.call(docEl); + } + } + + if (mapView && this.options.allowWheelOnFullscreen) { + mapView.map.set('scrollwheel', true); + } + } else { + cancelFullScreen.call(doc); + } + }, + + _canFullScreenBeEnabled: function () { + if (this._isInIframe()) { + var parentUrl = document.referrer; + if (parentUrl.search('https:') !== 0) { + return false; + } + } + return true; + }, + + _isInIframe: function () { + try { + return window.self !== window.top; + } catch (e) { + return true; + } + } +}); + +module.exports = FullScreen; diff --git a/src/util/backbone-abort-sync.js b/src/util/backbone-abort-sync.js new file mode 100644 index 0000000..ec759be --- /dev/null +++ b/src/util/backbone-abort-sync.js @@ -0,0 +1,17 @@ +var Backbone = require('backbone'); + +/** + * Abort ongoing request if it exists + */ +module.exports = function (method, model, options) { + var self = arguments[1]; + + if (this._xhr) { + this._xhr.abort(); + } + this._xhr = Backbone.sync.apply(this, arguments); + this._xhr.always(function () { + self._xhr = null; + }); + return this._xhr; +}; diff --git a/src/util/date-utils.js b/src/util/date-utils.js new file mode 100644 index 0000000..5cc700a --- /dev/null +++ b/src/util/date-utils.js @@ -0,0 +1,9 @@ +var dateUtils = {}; + +dateUtils.getLocalOffset = function () { + var date = new Date(); + // Return local timezone offset in seconds + return date.getTimezoneOffset() * (-60); +}; + +module.exports = dateUtils; diff --git a/src/util/formatter.js b/src/util/formatter.js new file mode 100644 index 0000000..dbb6b4a --- /dev/null +++ b/src/util/formatter.js @@ -0,0 +1,72 @@ +var _ = require('underscore'); +var d3TimeFormat = require('d3-time-format'); +var d3Format = require('d3-format'); + +var format = {}; + +var formatExponential = function (value) { + var template = _.template('<%= mantissa %>x10<%= decimals %>'); + var exp = value.toExponential(10); + var parts = exp.split('e'); + return template({ + mantissa: (parts[0] * 1).toFixed(1), + decimals: parts[1] || 0 + }); +}; + +format.formatNumber = function (value, unit) { + // we are using here the unary operator because parseInt fails to handle exponential number + var converted = +value; + if (isNaN(converted) || converted === 0) { + return value; + } + + value = converted; + + var format = d3Format.format('.2s'); + + var p = 0; + var absV = Math.abs(value); + + if (value > 1000) { + value = format(value) + (unit ? ' ' + unit : ''); + return value; + } + + if (absV < 0.01) { + value = formatExponential(value) + (unit ? ' ' + unit : ''); + return value; + } + + if (absV > 100) { + p = 0; + } else if (absV > 10) { + p = 1; + } else if (absV > 0.01) { + p = Math.min(Math.ceil(Math.abs(Math.log(absV) / Math.log(10))) + 2, 2); + } + + value = value.toFixed(p); + var m = value.match(/(\.0+)$/); + if (m) { + value = value.replace(m[0], ''); + } + + return value; +}; + +format.formatDate = function (value) { + return d3TimeFormat.timeFormat('%Y-%m-%d')(value); +}; + +format.formatValue = function (value) { + if (_.isNumber(value)) { + return format.formatNumber(value); + } + if (_.isDate(value)) { + return format.formatDate(value); + } + return value; +}; + +module.exports = format; diff --git a/src/util/get-object-value.js b/src/util/get-object-value.js new file mode 100644 index 0000000..b8c8c0d --- /dev/null +++ b/src/util/get-object-value.js @@ -0,0 +1,17 @@ +const _ = require('underscore'); + +/** + * Gets the value at path of object. If the resolved value is undefined, the defaultValue is returned in its place. + * @param {Object} object The object to query. + * @param {String} path The path of the property to get. + * @param {Any} defaultValue The value returned for undefined resolved values. + * @return {Any} Returns the resolved value. + */ +module.exports = function (object, path, defaultValue) { + var keys = path.split('.'); + var value = keys.reduce(function (a, b) { + return (a || {})[b]; + }, object); + + return _.isUndefined(value) ? defaultValue : value; +}; diff --git a/src/util/tile-error.tpl b/src/util/tile-error.tpl new file mode 100644 index 0000000..af8214e --- /dev/null +++ b/src/util/tile-error.tpl @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/vis/dataviews-tracker.js b/src/vis/dataviews-tracker.js new file mode 100644 index 0000000..c402ed3 --- /dev/null +++ b/src/vis/dataviews-tracker.js @@ -0,0 +1,17 @@ +var areAllDataviewsFetched = function (dataviewsCollection) { + return dataviewsCollection.length > 0 && dataviewsCollection.all(function (dataviewModel) { + return dataviewModel.isUnavailable() || dataviewModel.isFetched(); + }); +}; + +// dataviewsCollection is indded a Backbone Collection +var whenAllDataviewsFetched = function (dataviewsCollection, callback) { + var check = function () { + areAllDataviewsFetched(dataviewsCollection) && callback(); + }; + + dataviewsCollection.on('change:status', check); + check(); +}; + +module.exports = whenAllDataviewsFetched; diff --git a/src/vis/drawing-controller.js b/src/vis/drawing-controller.js new file mode 100644 index 0000000..17e9630 --- /dev/null +++ b/src/vis/drawing-controller.js @@ -0,0 +1,35 @@ +var DrawingController = function (mapView, map) { + this._map = map; + this._mapView = mapView; +}; + +DrawingController.prototype.enableDrawing = function (geometry) { + this.disableDrawing(); + + this._geometry = geometry; + this._map.addGeometry(this._geometry); + + this._map.disableInteractivity(); + this._mapView.on('click', this._onMapClicked, this); +}; + +DrawingController.prototype.disableDrawing = function () { + if (this._isDrawingEnabled()) { + this._geometry.remove(); + this._map.removeGeometry(this._geometry); + delete this._geometry; + + this._map.enableInteractivity(); + this._mapView.off('click', this._onMapClicked, this); + } +}; + +DrawingController.prototype._isDrawingEnabled = function () { + return !!this._geometry; +}; + +DrawingController.prototype._onMapClicked = function (event, latlng) { + this._geometry.update(latlng); +}; + +module.exports = DrawingController; diff --git a/src/vis/edition-controller.js b/src/vis/edition-controller.js new file mode 100644 index 0000000..1921f6b --- /dev/null +++ b/src/vis/edition-controller.js @@ -0,0 +1,37 @@ +var EditionController = function (mapView, map) { + this._map = map; + this._mapView = mapView; +}; + +EditionController.prototype.enableEdition = function (geometry) { + this.disableEdition(); + + this._geometry = geometry; + this._map.addGeometry(this._geometry); + + this._werePopupsEnabled = this._map.arePopupsEnabled(); + this._map.disablePopups(); +}; + +EditionController.prototype.disableEdition = function () { + if (this._isEditionEnabled()) { + this._geometry.remove(); + this._map.removeGeometry(this._geometry); + delete this._geometry; + this._reEnableOrDisablePopups(); + } +}; + +EditionController.prototype._isEditionEnabled = function () { + return !!this._geometry; +}; + +EditionController.prototype._reEnableOrDisablePopups = function () { + if (this._werePopupsEnabled) { + this._map.enablePopups(); + } else { + this._map.disablePopups(); + } +}; + +module.exports = EditionController; diff --git a/src/vis/geometry-management-controller.js b/src/vis/geometry-management-controller.js new file mode 100644 index 0000000..e17b656 --- /dev/null +++ b/src/vis/geometry-management-controller.js @@ -0,0 +1,48 @@ +var DrawingController = require('./drawing-controller'); +var EditionController = require('./edition-controller'); + +var GeometryManagementController = function (mapView, map) { + this._map = map; + + this._drawingController = new DrawingController(mapView, map); + this._editionController = new EditionController(mapView, map); +}; + +GeometryManagementController.prototype.start = function () { + this._map.on('enterDrawingMode', this._enterDrawingMode, this); + this._map.on('exitDrawingMode', this._exitDrawingMode, this); + this._map.on('enterEditMode', this._enterEditMode, this); + this._map.on('exitEditMode', this._exitEditMode, this); +}; + +GeometryManagementController.prototype.stop = function () { + this._exitDrawingMode(); + this._exitEditMode(); + + this._map.off('enterDrawingMode', this._enterDrawingMode, this); + this._map.off('exitDrawingMode', this._exitDrawingMode, this); + this._map.off('enterEditMode', this._enterEditMode, this); + this._map.off('exitEditMode', this._exitEditMode, this); +}; + +GeometryManagementController.prototype._enterDrawingMode = function (geometry) { + this._exitDrawingMode(); + this._exitEditMode(); + this._drawingController.enableDrawing(geometry); +}; + +GeometryManagementController.prototype._exitDrawingMode = function () { + this._drawingController.disableDrawing(); +}; + +GeometryManagementController.prototype._enterEditMode = function (geometry) { + this._exitDrawingMode(); + this._exitEditMode(); + this._editionController.enableEdition(geometry); +}; + +GeometryManagementController.prototype._exitEditMode = function () { + this._editionController.disableEdition(); +}; + +module.exports = GeometryManagementController; diff --git a/src/vis/image/queue.js b/src/vis/image/queue.js new file mode 100644 index 0000000..a75fe10 --- /dev/null +++ b/src/vis/image/queue.js @@ -0,0 +1,45 @@ +var Queue = function () { + // callback storage + this._methods = []; + + // reference to the response + this._response = null; + + // all queues start off unflushed + this._flushed = false; +}; + +Queue.prototype = { + + // adds callbacks to the queue + add: function (fn) { + // if the queue had been flushed, return immediately + if (this._flushed) { + // otherwise push it on the queue + fn(this._response); + } else { + this._methods.push(fn); + } + }, + + flush: function (resp) { + // flush only ever happens once + if (this._flushed) { + return; + } + + // store the response for subsequent calls after flush() + this._response = resp; + + // mark that it's been flushed + this._flushed = true; + + // shift 'em out and call 'em back + while (this._methods[0]) { + this._methods.shift()(resp); + } + } + +}; + +module.exports = Queue; diff --git a/src/vis/infowindow-manager.js b/src/vis/infowindow-manager.js new file mode 100644 index 0000000..1905244 --- /dev/null +++ b/src/vis/infowindow-manager.js @@ -0,0 +1,144 @@ +var Engine = require('../../src/engine'); + +/** + * Manages the infowindows for a map. It listens to events triggered by a + * CartoDBLayerGroupView and updates models accordingly + */ +var InfowindowManager = function (deps, options) { + deps = deps || {}; + options = options || {}; + if (!deps.engine) throw new Error('engine is required'); + if (!deps.mapModel) throw new Error('mapModel is required'); + if (!deps.infowindowModel) throw new Error('infowindowModel is required'); + if (!deps.tooltipModel) throw new Error('tooltipModel is required'); + + this._engine = deps.engine; + this._mapModel = deps.mapModel; + this._infowindowModel = deps.infowindowModel; + this._tooltipModel = deps.tooltipModel; + this._showEmptyFields = !!options.showEmptyFields; + + this._cartoDBLayerGroupView = null; + this._cartoDBLayerModel = null; + this.currentFeatureUniqueId = null; + + this._mapModel.on('change:popupsEnabled', this._onPopupsEnabledChanged, this); +}; + +InfowindowManager.prototype.start = function (cartoDBLayerGroupView) { + this._cartoDBLayerGroupView = cartoDBLayerGroupView; + cartoDBLayerGroupView.on('featureClick', this._onFeatureClicked, this); +}; + +InfowindowManager.prototype.stop = function () { + if (this._cartoDBLayerGroupView) { + this._cartoDBLayerGroupView.off('featureClick', this._onFeatureClicked, this); + delete this._cartoDBLayerGroupView; + } + if (this._cartoDBLayerModel) { + this._unbindLayerModel(); + delete this._cartoDBLayerModel; + } +}; + +InfowindowManager.prototype._onFeatureClicked = function (featureClickEvent) { + this._unbindLayerModel(); + + this._cartoDBLayerModel = featureClickEvent.layer; + var featureData = featureClickEvent.feature; + var featureId = featureData.cartodb_id; + var latLng = featureClickEvent.latlng; + + if (!this._mapModel.arePopupsEnabled() || !this._cartoDBLayerModel.infowindow.hasTemplate()) { + return; + } + + this._tooltipModel.hide(); + this._updateInfowindowModel(this._cartoDBLayerModel.infowindow); + this._infowindowModel.setLatLng(latLng); + this._showInfowindow(featureId); + this._fetchAttributes(featureId); +}; + +InfowindowManager.prototype._showInfowindow = function (featureId) { + this._bindLayerModel(); + this._infowindowModel.show(); + this._infowindowModel.setCurrentFeatureId(featureId); +}; + +InfowindowManager.prototype._hideInfowindow = function () { + this._unbindLayerModel(); + this._infowindowModel.hide(); + this._infowindowModel.unsetCurrentFeatureId(); +}; + +InfowindowManager.prototype._fetchAttributes = function (featureId) { + var layerIndex = this._cartoDBLayerGroupView.model.getIndexOfLayerInLayerGroup(this._cartoDBLayerModel); + var currentFeatureUniqueId = this._generateFeatureUniqueId(layerIndex, featureId); + + this._currentFeatureId = featureId || this._currentFeatureId; + this._infowindowModel.setLoading(); + this.currentFeatureUniqueId = currentFeatureUniqueId; + + this._cartoDBLayerGroupView.model.fetchAttributes(layerIndex, this._currentFeatureId, function (attributes) { + if (currentFeatureUniqueId !== this.currentFeatureUniqueId) { + return; + } + + if (attributes) { + this._infowindowModel.updateContent(attributes, { + showEmptyFields: this._showEmptyFields + }); + } else { + this._infowindowModel.setError(); + } + }.bind(this)); +}; + +InfowindowManager.prototype._bindLayerModel = function () { + this._cartoDBLayerModel.on('destroy', this._hideInfowindow, this); + this._cartoDBLayerModel.on('change:visible', this._hideInfowindow, this); + this._cartoDBLayerModel.infowindow.on('change', this._updateInfowindowModel, this); + this._cartoDBLayerModel.infowindow.fields.on('reset', this._onInfowindowTemplateFieldsReset, this); + this._engine.on(Engine.Events.RELOAD_SUCCESS, this._onReloaded, this); // TODO: Add tests to check _onReloaded is being called. +}; + +InfowindowManager.prototype._unbindLayerModel = function () { + if (this._cartoDBLayerModel) { + this._cartoDBLayerModel.off('destroy', this._hideInfowindow, this); + this._cartoDBLayerModel.off('change:visible', this._hideInfowindow, this); + this._cartoDBLayerModel.infowindow.off('change', this._updateInfowindowModel, this); + this._cartoDBLayerModel.infowindow.fields.off('reset', this._onInfowindowTemplateFieldsReset, this); + this._engine.off(Engine.Events.RELOAD_SUCCESS, this._onReloaded, this); + } +}; + +InfowindowManager.prototype._updateInfowindowModel = function (infowindowTemplate) { + this._infowindowModel.setInfowindowTemplate(infowindowTemplate); +}; + +InfowindowManager.prototype._onInfowindowTemplateFieldsReset = function () { + if (this._cartoDBLayerModel.infowindow.hasFields()) { + this._updateInfowindowModel(this._cartoDBLayerModel.infowindow); + } else { + this._hideInfowindow(); + } +}; + +InfowindowManager.prototype._onReloaded = function () { + if (this._cartoDBLayerModel && this._cartoDBLayerModel.infowindow.hasFields()) { + this._fetchAttributes(); + } +}; + +InfowindowManager.prototype._onPopupsEnabledChanged = function () { + if (this._mapModel.arePopupsDisabled()) { + this._hideInfowindow(); + } +}; + +InfowindowManager.prototype._generateFeatureUniqueId = function (layerId, featureId) { + return [layerId, featureId].join('_'); +}; + +module.exports = InfowindowManager; diff --git a/src/vis/layers-factory.js b/src/vis/layers-factory.js new file mode 100644 index 0000000..be59750 --- /dev/null +++ b/src/vis/layers-factory.js @@ -0,0 +1,153 @@ +var _ = require('underscore'); +var log = require('cdb.log'); +var TileLayer = require('../geo/map/tile-layer'); +var WMSLayer = require('../geo/map/wms-layer'); +var GMapsBaseLayer = require('../geo/map/gmaps-base-layer'); +var PlainLayer = require('../geo/map/plain-layer'); +var CartoDBLayer = require('../geo/map/cartodb-layer'); +var TorqueLayer = require('../geo/map/torque-layer'); + +/* + * if we are using http and the tiles of base map need to be fetched from + * https try to fix it + */ +var HTTPS_TO_HTTP = { + 'https://dnv9my2eseobd.cloudfront.net/': 'http://a.tiles.mapbox.com/', + 'https://maps.nlp.nokia.com/': 'http://maps.nlp.nokia.com/', + 'https://tile.stamen.com/': 'http://tile.stamen.com/', + 'https://{s}.maps.nlp.nokia.com/': 'http://{s}.maps.nlp.nokia.com/', + 'https://cartocdn_{s}.global.ssl.fastly.net/': 'http://{s}.api.cartocdn.com/', + 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/': 'http://{s}.basemaps.cartocdn.com/' +}; + +function transformToHTTP (tilesTemplate) { + for (var url in HTTPS_TO_HTTP) { + if (tilesTemplate.indexOf(url) !== -1) { + return tilesTemplate.replace(url, HTTPS_TO_HTTP[url]); + } + } + return tilesTemplate; +} + +function transformToHTTPS (tilesTemplate) { + for (var url in HTTPS_TO_HTTP) { + var httpsUrl = HTTPS_TO_HTTP[url]; + if (tilesTemplate.indexOf(httpsUrl) !== -1) { + return tilesTemplate.replace(httpsUrl, url); + } + } + return tilesTemplate; +} + +function checkProperties (obj, requiredProperties) { + var missingProperties = _.select(requiredProperties, function (property) { + var properties = property.split('|'); + return _.all(properties, function (property) { + return obj[property] === undefined; + }); + }); + if (missingProperties.length) { + throw new Error('The following attributes are missing: ' + missingProperties.join(',')); + } +} + +var LAYER_CONSTRUCTORS = { + tiled: function (attrs, options) { + checkProperties(attrs, ['urlTemplate']); + + attrs.urlTemplate = LayersFactory.isHttps() + ? transformToHTTPS(attrs.urlTemplate) + : transformToHTTP(attrs.urlTemplate); + + return new TileLayer(attrs, { engine: options.engine }); + }, + + wms: function (attrs, options) { + checkProperties(attrs, ['urlTemplate']); + + return new WMSLayer(attrs); + }, + + gmapsbase: function (attrs, options) { + checkProperties(attrs, ['baseType']); + + return new GMapsBaseLayer(attrs); + }, + + plain: function (attrs, options) { + checkProperties(attrs, ['image|color']); + + return new PlainLayer(attrs, { engine: options.engine }); + }, + + background: function (data, options) { + return new PlainLayer(data, { engine: options.engine }); + }, + + cartodb: function (attrs, options) { + // TODO: Once https://github.com/CartoDB/cartodb/issues/12885 is merged, + // we should make 'source' attribute required again and make sure it's + // always populated: + // checkProperties(attrs, ['source', 'cartocss']); + // CartoDBLayer._checkSourceAttribute(attrs.source); + checkProperties(attrs, ['cartocss']); + + return new CartoDBLayer(attrs, { engine: options.engine }); + }, + + torque: function (attrs, options) { + // TODO: Once https://github.com/CartoDB/cartodb/issues/12885 is merged, + // we should make 'source' attribute required again and make sure it's + // populated: + // checkProperties(attrs, ['source', 'cartocss']); + // CartoDBLayer._checkSourceAttribute(attrs.source); + checkProperties(attrs, ['cartocss']); + + var windshaftSettings = options.windshaftSettings; + + attrs = _.extend(attrs, { + user_name: windshaftSettings.userName, + maps_api_template: windshaftSettings.urlTemplate, + client: windshaftSettings.client, + api_key: windshaftSettings.apiKey, + auth_token: windshaftSettings.authToken + }); + + if (windshaftSettings.templateName) { + attrs = _.extend(attrs, { + named_map: { + name: windshaftSettings.templateName + } + }); + } + + return new TorqueLayer(attrs, { engine: options.engine }); + } +}; + +function LayersFactory (deps) { + if (!deps.engine) throw new Error('engine is required'); + if (!deps.windshaftSettings) throw new Error('windshaftSettings is required'); + + this._engine = deps.engine; + this._windshaftSettings = deps.windshaftSettings; +} + +LayersFactory.prototype.createLayer = function (type, attrs) { + var LayerConstructor = LAYER_CONSTRUCTORS[type.toLowerCase()]; + if (!LayerConstructor) { + log.error("error creating layer of type '" + type + "'"); + return null; + } + + return LayerConstructor(attrs, { + windshaftSettings: this._windshaftSettings, + engine: this._engine + }); +}; + +LayersFactory.isHttps = function () { + return (window && window.location.protocol && window.location.protocol === 'https:') || false; +}; + +module.exports = LayersFactory; diff --git a/src/vis/map-cursor-manager.js b/src/vis/map-cursor-manager.js new file mode 100644 index 0000000..889bf40 --- /dev/null +++ b/src/vis/map-cursor-manager.js @@ -0,0 +1,85 @@ +var _ = require('underscore'); + +/** + * Changes the mouse pointer based on feature events and status + * of map's interactivity. + * @param {Object} deps Dependencies + */ +var MapCursorManager = function (deps) { + if (!deps.mapView) throw new Error('mapView is required'); + if (!deps.mapModel) throw new Error('mapModel is required'); + + this._mapView = deps.mapView; + this._mapModel = deps.mapModel; + + // Map to keep track of clickable layers that are being feature overed + this._clickableLayersBeingFeatureOvered = {}; +}; + +MapCursorManager.prototype.start = function (cartoDBLayerGroupView) { + this._cartoDBLayerGroupView = cartoDBLayerGroupView; + this._cartoDBLayerGroupView.on('featureOver', this._onFeatureOver, this); + this._cartoDBLayerGroupView.on('featureOut', this._onFeatureOut, this); +}; + +MapCursorManager.prototype.stop = function (cartoDBLayerGroupView) { + if (this._cartoDBLayerGroupView) { + this._cartoDBLayerGroupView.off('featureOver', this._onFeatureOver, this); + this._cartoDBLayerGroupView.off('featureOut', this._onFeatureOut, this); + delete this._cartoDBLayerGroupView; + } +}; + +MapCursorManager.prototype._onFeatureOver = function (featureEvent) { + if (this._isLayerClickable(featureEvent.layer) && + !this._isLayerBeingFeatureOvered(featureEvent.layer)) { + this._markLayerAsFeatureOvered(featureEvent.layer); + this._updateMousePointer(); + } +}; + +MapCursorManager.prototype._onFeatureOut = function (featureEvent) { + if (this._isLayerBeingFeatureOvered(featureEvent.layer)) { + this._unmarkLayerAsFeatureOvered(featureEvent.layer); + this._updateMousePointer(); + } +}; + +MapCursorManager.prototype._onLayerVisibilityChanged = function (layerModel) { + if (!layerModel.isVisible()) { + this._unmarkLayerAsFeatureOvered(layerModel); + this._updateMousePointer(); + } +}; + +MapCursorManager.prototype._updateMousePointer = function () { + if (this._isAnyClickableLayerBeingFeatureOvered()) { + this._mapView.setCursor('pointer'); + } else { + this._mapView.setCursor('auto'); + } +}; + +MapCursorManager.prototype._markLayerAsFeatureOvered = function (layerModel) { + this._clickableLayersBeingFeatureOvered[layerModel.cid] = layerModel; + layerModel.once('change:visible', this._onLayerVisibilityChanged, this); +}; + +MapCursorManager.prototype._unmarkLayerAsFeatureOvered = function (layerModel) { + delete this._clickableLayersBeingFeatureOvered[layerModel.cid]; + layerModel.off('change:visible', this._onLayerVisibilityChanged, this); +}; + +MapCursorManager.prototype._isLayerClickable = function (layerModel) { + return this._mapModel.isFeatureInteractivityEnabled() || (this._mapModel.arePopupsEnabled() && layerModel.isInfowindowEnabled()); +}; + +MapCursorManager.prototype._isLayerBeingFeatureOvered = function (layerModel) { + return !!this._clickableLayersBeingFeatureOvered[layerModel.cid]; +}; + +MapCursorManager.prototype._isAnyClickableLayerBeingFeatureOvered = function () { + return !_.isEmpty(this._clickableLayersBeingFeatureOvered); +}; + +module.exports = MapCursorManager; diff --git a/src/vis/map-events-manager.js b/src/vis/map-events-manager.js new file mode 100644 index 0000000..9df7be3 --- /dev/null +++ b/src/vis/map-events-manager.js @@ -0,0 +1,54 @@ +/** + * Listens to feature events and "forwards" them to the map model + * when map's feature interactivity is enabled. + * @param {Object} deps Dependencies + */ +var MapModelEventsManager = function (deps) { + if (!deps.mapModel) throw new Error('mapModel is required'); + + this._mapModel = deps.mapModel; +}; + +MapModelEventsManager.prototype.start = function (cartoDBLayerGroupView) { + this._cartoDBLayerGroupView = cartoDBLayerGroupView; + this._cartoDBLayerGroupView.on('featureOver', this._onFeatureOver, this); + this._cartoDBLayerGroupView.on('featureClick', this._onFeatureClick, this); + this._cartoDBLayerGroupView.on('featureOut', this._onFeatureOut, this); + this._cartoDBLayerGroupView.on('featureError', this._onFeatureError, this); +}; + +MapModelEventsManager.prototype.stop = function () { + if (this._cartoDBLayerGroupView) { + this._cartoDBLayerGroupView.off('featureOver', this._onFeatureOver, this); + this._cartoDBLayerGroupView.off('featureClick', this._onFeatureClick, this); + this._cartoDBLayerGroupView.off('featureOut', this._onFeatureOut, this); + this._cartoDBLayerGroupView.off('featureError', this._onFeatureError, this); + delete this._cartoDBLayerGroupView; + } +}; + +MapModelEventsManager.prototype._onFeatureOver = function (event) { + if (this._mapModel.isFeatureInteractivityEnabled()) { + this._mapModel.trigger('featureOver', event); + } +}; + +MapModelEventsManager.prototype._onFeatureClick = function (event) { + if (this._mapModel.isFeatureInteractivityEnabled()) { + this._mapModel.trigger('featureClick', event); + } +}; + +MapModelEventsManager.prototype._onFeatureOver = function (event) { + if (this._mapModel.isFeatureInteractivityEnabled()) { + this._mapModel.trigger('featureOver', event); + } +}; + +MapModelEventsManager.prototype._onFeatureError = function (event) { + if (this._mapModel.isFeatureInteractivityEnabled()) { + this._mapModel.trigger('featureError', event); + } +}; + +module.exports = MapModelEventsManager; diff --git a/src/vis/overlays-factory.js b/src/vis/overlays-factory.js new file mode 100644 index 0000000..6e7da2d --- /dev/null +++ b/src/vis/overlays-factory.js @@ -0,0 +1,50 @@ +var _ = require('underscore'); +var log = require('cdb.log'); + +var OVERLAYS = require('./overlays'); + +var OverlaysFactory = function (opts) { + opts = opts || {}; + if (!opts.mapModel) throw new Error('mapModel is required'); + if (!opts.mapView) throw new Error('mapView is required'); + if (!opts.visView) throw new Error('visView is required'); + + this._mapModel = opts.mapModel; + this._mapView = opts.mapView; + this._visView = opts.visView; +}; + +OverlaysFactory._constructors = {}; + +OverlaysFactory.register = function (type, creatorFn) { + this._constructors[type] = creatorFn; +}; + +OverlaysFactory.prototype.create = function (type, data) { + var overlayConstructor = this.constructor._constructors[type]; + if (!overlayConstructor) { + log.log("Overlays of type '" + type + "' are not supported anymore"); + return; + } + + data.options = typeof data.options === 'string' ? JSON.parse(data.options) : data.options; + data.options = data.options || {}; + var overlay = overlayConstructor(data, { + mapModel: this._mapModel, + mapView: this._mapView, + visView: this._visView + }); + + if (overlay) { + overlay.type = type; + return overlay; + } + + return false; +}; + +_.each(OVERLAYS, function (value, key) { + OverlaysFactory.register(key, value); +}); + +module.exports = OverlaysFactory; diff --git a/src/vis/overlays/attribution.js b/src/vis/overlays/attribution.js new file mode 100644 index 0000000..6b9db99 --- /dev/null +++ b/src/vis/overlays/attribution.js @@ -0,0 +1,13 @@ +var AttributionView = require('../../geo/ui/attribution/attribution-view'); + +var AttributionOverlay = function (_data, opts) { + if (!opts.mapModel) throw new Error('mapModel is required'); + + var view = new AttributionView({ + map: opts.mapModel + }); + + return view.render(); +}; + +module.exports = AttributionOverlay; diff --git a/src/vis/overlays/custom.js b/src/vis/overlays/custom.js new file mode 100644 index 0000000..f1381cd --- /dev/null +++ b/src/vis/overlays/custom.js @@ -0,0 +1,7 @@ +var CustomOverlay = function (data, opts) { + var view = data; + + return view.render(); +}; + +module.exports = CustomOverlay; diff --git a/src/vis/overlays/fullscreen.js b/src/vis/overlays/fullscreen.js new file mode 100644 index 0000000..acf17d8 --- /dev/null +++ b/src/vis/overlays/fullscreen.js @@ -0,0 +1,27 @@ +var _ = require('underscore'); +var FullScreen = require('../../ui/common/fullscreen/fullscreen-view'); +var Template = require('../../core/template'); + +var FullscreenOverlay = function (data, opts) { + if (!opts.mapView) throw new Error('mapView is required'); + if (!opts.visView) throw new Error('visView is required'); + + var options = _.extend(data, { + doc: opts.visView.$el.find('> div').get(0), + allowWheelOnFullscreen: false, + mapView: opts.mapView + }); + + if (data.template) { + options.template = Template.compile( + data.template, + data.templateType || 'mustache' + ); + } + + var view = new FullScreen(options); + + return view.render(); +}; + +module.exports = FullscreenOverlay; diff --git a/src/vis/overlays/index.js b/src/vis/overlays/index.js new file mode 100644 index 0000000..915ae39 --- /dev/null +++ b/src/vis/overlays/index.js @@ -0,0 +1,12 @@ +module.exports = { + attribution: require('./attribution'), + custom: require('./custom'), + fullscreen: require('./fullscreen'), + layer_selector: require('./layer_selector'), + limits: require('./limits'), + tiles: require('./tiles'), + loader: require('./loader'), + logo: require('./logo'), + search: require('./search'), + zoom: require('./zoom') +}; diff --git a/src/vis/overlays/layer_selector.js b/src/vis/overlays/layer_selector.js new file mode 100644 index 0000000..0bb569f --- /dev/null +++ b/src/vis/overlays/layer_selector.js @@ -0,0 +1,3 @@ +var LayerSelectorOverlay = function (data, opts) {}; + +module.exports = LayerSelectorOverlay; diff --git a/src/vis/overlays/limits.js b/src/vis/overlays/limits.js new file mode 100644 index 0000000..a3ec0bd --- /dev/null +++ b/src/vis/overlays/limits.js @@ -0,0 +1,13 @@ +var LimitsView = require('../../geo/ui/limits/limits-view'); + +var LimitsOverlay = function (data, opts) { + if (!opts.mapModel) throw new Error('mapModel is required'); + + var view = new LimitsView({ + map: opts.mapModel + }); + + return view.render(); +}; + +module.exports = LimitsOverlay; diff --git a/src/vis/overlays/loader.js b/src/vis/overlays/loader.js new file mode 100644 index 0000000..c518d2b --- /dev/null +++ b/src/vis/overlays/loader.js @@ -0,0 +1,9 @@ +var TilesLoader = require('../../geo/ui/tiles-loader'); + +var LoaderOverlay = function (data) { + var view = new TilesLoader(); + + return view.render(); +}; + +module.exports = LoaderOverlay; diff --git a/src/vis/overlays/logo.js b/src/vis/overlays/logo.js new file mode 100644 index 0000000..4c9c632 --- /dev/null +++ b/src/vis/overlays/logo.js @@ -0,0 +1,9 @@ +var LogoView = require('../../geo/ui/logo-view'); + +var LogoOverlay = function (data, opts) { + var view = new LogoView(); + + return view.render(); +}; + +module.exports = LogoOverlay; diff --git a/src/vis/overlays/search.js b/src/vis/overlays/search.js new file mode 100644 index 0000000..65762a2 --- /dev/null +++ b/src/vis/overlays/search.js @@ -0,0 +1,22 @@ +var _ = require('underscore'); +var Search = require('../../geo/ui/search/search'); +var Template = require('../../core/template'); + +var SearchOverlay = function (data, opts) { + if (!opts.mapView) throw new Error('mapView is required'); + if (!opts.mapModel) throw new Error('mapModel is required'); + + var options = _.extend(data, { + mapView: opts.mapView, + model: opts.mapModel + }); + + if (data.template) { + options.template = Template.compile(data.template, data.templateType || 'mustache'); + } + + var overlay = new Search(options); + return overlay.render(); +}; + +module.exports = SearchOverlay; diff --git a/src/vis/overlays/tiles.js b/src/vis/overlays/tiles.js new file mode 100644 index 0000000..73bc343 --- /dev/null +++ b/src/vis/overlays/tiles.js @@ -0,0 +1,13 @@ +var TilesView = require('../../geo/ui/tiles/tiles-view'); + +var TilesOverlay = function (data, opts) { + if (!opts.mapModel) throw new Error('mapModel is required'); + + var view = new TilesView({ + map: opts.mapModel + }); + + return view.render(); +}; + +module.exports = TilesOverlay; diff --git a/src/vis/overlays/zoom.js b/src/vis/overlays/zoom.js new file mode 100644 index 0000000..f8776e7 --- /dev/null +++ b/src/vis/overlays/zoom.js @@ -0,0 +1,13 @@ +var Zoom = require('../../geo/ui/zoom/zoom-view'); + +var ZoomOverlay = function (data, opts) { + if (!opts.mapModel) throw new Error('mapModel is required'); + + var view = new Zoom({ + model: opts.mapModel + }); + + return view.render(); +}; + +module.exports = ZoomOverlay; diff --git a/src/vis/settings.js b/src/vis/settings.js new file mode 100644 index 0000000..2538689 --- /dev/null +++ b/src/vis/settings.js @@ -0,0 +1,10 @@ +var Backbone = require('backbone'); + +var UISettings = Backbone.Model.extend({ + defaults: { + showLegends: true, + showLayerSelector: false + } +}); + +module.exports = UISettings; diff --git a/src/vis/tooltip-manager.js b/src/vis/tooltip-manager.js new file mode 100644 index 0000000..b9d4825 --- /dev/null +++ b/src/vis/tooltip-manager.js @@ -0,0 +1,97 @@ +var _ = require('underscore'); + +/** + * Manages the tooltips for a map. It listens to events triggered by a + * CartoDBLayerGroupView and updates models accordingly + */ +var TooltipManager = function (deps) { + if (!deps.mapModel) throw new Error('mapModel is required'); + if (!deps.tooltipModel) throw new Error('tooltipModel is required'); + if (!deps.infowindowModel) throw new Error('infowindowModel is required'); + + this._mapModel = deps.mapModel; + this._tooltipModel = deps.tooltipModel; + this._infowindowModel = deps.infowindowModel; + + this._layersBeingFeaturedOvered = {}; +}; + +TooltipManager.prototype.start = function (cartoDBLayerGroupView) { + this._cartoDBLayerGroupView = cartoDBLayerGroupView; + this._cartoDBLayerGroupView.on('featureOver', this._onFeatureOvered, this); + this._cartoDBLayerGroupView.on('featureOut', this._onFeatureOut, this); +}; + +TooltipManager.prototype.stop = function () { + if (this._cartoDBLayerGroupView) { + this._cartoDBLayerGroupView.off('featureOver', this._onFeatureOvered, this); + this._cartoDBLayerGroupView.off('featureOut', this._onFeatureOut, this); + } +}; + +TooltipManager.prototype._onFeatureOvered = function (featureOverEvent) { + var layerModel = featureOverEvent.layer; + var featureData = featureOverEvent.feature; + var featureId = featureData.cartodb_id; + + if (!layerModel) { + throw new Error('featureOver event for layer ' + featureOverEvent.layerIndex + ' was captured but layerModel coudn\'t be retrieved'); + } + + var showTooltip = this._mapModel.arePopupsEnabled() && layerModel.tooltip.hasTemplate() && + !this._isFeatureInfowindowOpen(layerModel, featureId); + if (showTooltip) { + this._markLayerAsFeatureOvered(layerModel); + this._tooltipModel.setTooltipTemplate(layerModel.tooltip); + this._tooltipModel.setPosition(featureOverEvent.position); + this._tooltipModel.updateContent(featureData); + this._showTooltip(layerModel); + } else { + this._unmarkLayerAsFeatureOvered(layerModel); + this._hideTooltip(layerModel); + } +}; + +TooltipManager.prototype._onFeatureOut = function (featureOutEvent) { + var layerModel = featureOutEvent.layer; + this._unmarkLayerAsFeatureOvered(layerModel); + if (this._noLayerIsBeingFeatureOvered()) { + this._hideTooltip(layerModel); + } +}; + +TooltipManager.prototype._showTooltip = function (layerModel) { + this._tooltipModel.show(); + layerModel.on('change:visible', this._onLayerVisibilityChanged, this); +}; + +TooltipManager.prototype._hideTooltip = function (layerModel) { + this._tooltipModel.hide(); + layerModel.off('change:visible', this._onLayerVisibilityChanged, this); +}; + +TooltipManager.prototype._onLayerVisibilityChanged = function (layerModel) { + this._hideTooltip(layerModel); +}; + +TooltipManager.prototype._markLayerAsFeatureOvered = function (layerModel) { + this._layersBeingFeaturedOvered[layerModel.cid] = true; +}; + +TooltipManager.prototype._unmarkLayerAsFeatureOvered = function (layerModel) { + this._layersBeingFeaturedOvered[layerModel.cid] = false; +}; + +TooltipManager.prototype._noLayerIsBeingFeatureOvered = function () { + return _.all(this._layersBeingFeaturedOvered, function (value, key) { + return !value; + }, this); +}; + +TooltipManager.prototype._isFeatureInfowindowOpen = function (layerModel, featureId) { + return featureId && this._infowindowModel.getCurrentFeatureId() === featureId && + this._infowindowModel.isVisible() && + this._infowindowModel.hasInfowindowTemplate(layerModel.infowindow); +}; + +module.exports = TooltipManager; diff --git a/src/vis/vis-view.js b/src/vis/vis-view.js new file mode 100644 index 0000000..4bddee4 --- /dev/null +++ b/src/vis/vis-view.js @@ -0,0 +1,132 @@ +var _ = require('underscore'); +var $ = require('jquery'); +var View = require('../core/view'); +var MapViewFactory = require('../geo/map-view-factory'); +var LegendsView = require('../geo/ui/legends/legends-view'); +var OverlaysView = require('../geo/ui/overlays-view'); + +/** + * Visualization creation + */ +var Vis = View.extend({ + initialize: function (options) { + this.model.once('reloaded', this.render, this); + this.model.on('invalidateSize', this._invalidateSize, this); + + this._overlaysCollection = this.model.overlaysCollection; + + this.settingsModel = options.settingsModel; + + _.bindAll(this, '_onResize'); + }, + + render: function () { + this.model.map.off('change:provider', this.render, this); + this.model.map.on('change:provider', this.render, this); + + this._cleanMapView(); + this._renderMapView(); + + this._cleanLegendsView(); + this._renderLegendsView(); + + this._cleanOverlaysView(); + this._renderOverlaysView(); + + // If a CartoDB embed map is hidden by default, its + // height is 0 and it will need to recalculate its size + // and re-center again. + // We will wait until it is resized and then apply + // the center provided in the parameters and the + // correct size. + var mapH = this.$el.outerHeight(); + if (mapH === 0) { + $(window).bind('resize', this._onResize); + } + }, + + _renderMapView: function () { + this.mapView = MapViewFactory.createMapView(this.model.map.get('provider'), this.model); + // Add the element to the DOM before the native map is created + this.$el.html(this.mapView.el); + + // Bind events before the view is rendered and layer views are added to the map + this.listenTo(this.mapView, 'newLayerView', this._bindLayerViewToLoader); + // Trigger click event in map model + this.listenTo(this.mapView, 'click', function () { + this.model.trigger('click'); + }); + this.mapView.render(); + }, + + _cleanMapView: function () { + this.mapView && this.mapView.clean(); + }, + + _renderLegendsView: function () { + this._legendsView = new LegendsView({ + layersCollection: this.model.map.layers, + settingsModel: this.settingsModel + }); + + this.$el.append(this._legendsView.render().$el); + this.addView(this._legendsView); + + var embedLegends = $('.js-embed-legends'); + + if (embedLegends.length) { + this._embedLegendsView = new LegendsView({ + layersCollection: this.model.map.layers, + settingsModel: this.settingsModel + }); + + embedLegends.append(this._embedLegendsView.render().$el); + this.addView(this._embedLegendsView); + } + }, + + _cleanLegendsView: function () { + this._legendsView && this._legendsView.clean(); + }, + + _renderOverlaysView: function () { + this._overlaysView = new OverlaysView({ + engine: this.model._engine, + visView: this, + mapModel: this.model.map, + mapView: this.mapView, + overlaysCollection: this._overlaysCollection + }); + this.$el.append(this._overlaysView.render().$el); + }, + + _cleanOverlaysView: function () { + this._overlaysView && this._overlaysView.clean(); + }, + + _bindLayerViewToLoader: function (layerView) { + layerView.bind('load', function () { + this.model.untrackLoadingObject(layerView); + }, this); + layerView.bind('loading', function () { + this.model.trackLoadingObject(layerView); + }, this); + }, + + _invalidateSize: function () { + this.mapView.invalidateSize(); + }, + + _onResize: function () { + $(window).unbind('resize', this._onResize); + + var self = this; + // This timeout is necessary due to GMaps needs time + // to load tiles and recalculate its bounds :S + setTimeout(function () { + self.model.invalidateSize(); + }, 150); + } +}); + +module.exports = Vis; diff --git a/src/vis/vis.js b/src/vis/vis.js new file mode 100644 index 0000000..d310af2 --- /dev/null +++ b/src/vis/vis.js @@ -0,0 +1,416 @@ +var _ = require('underscore'); +var Backbone = require('backbone'); +var util = require('../core/util'); +var Map = require('../geo/map'); +var DataviewsFactory = require('../dataviews/dataviews-factory'); +var AnalysisService = require('../analysis/analysis-service'); +var LayersFactory = require('./layers-factory'); +var SettingsModel = require('./settings'); +var whenAllDataviewsFetched = require('./dataviews-tracker'); +var RenderModes = require('../geo/render-modes'); +var Engine = require('../engine'); + +var STATE_INIT = 'init'; // vis hasn't been sent to Windshaft +var STATE_OK = 'ok'; // vis has been sent to Windshaft and everything is ok +var STATE_ERROR = 'error'; // vis has been sent to Windshaft and there were some issues + +var VisModel = Backbone.Model.extend({ + defaults: { + loading: false, + showEmptyInfowindowFields: false, + showLimitErrors: false, + state: STATE_INIT + }, + + initialize: function () { + this._loadingObjects = []; + this.overlaysCollection = new Backbone.Collection(); + this.settings = new SettingsModel(); + this._instantiateMapWasCalled = false; + }, + + getStaticImageURL: function (options) { + options = _.defaults({}, options, { + zoom: 4, + lat: 0, + lng: 0, + width: 300, + height: 300, + format: 'png' + }); + + var url; + var urlTemplate = this.layerGroupModel.getStaticImageURLTemplate(); + if (urlTemplate) { + url = urlTemplate + .replace('{z}', options.zoom) + .replace('{lat}', options.lat) + .replace('{lng}', options.lng) + .replace('{width}', options.width) + .replace('{height}', options.height) + .replace('{format}', options.format); + } + return url; + }, + + done: function (callback) { + this._doneCallback = callback; + return this; + }, + + setOk: function () { + // Invoke this._doneCallback if present, the first time + // the vis is instantiated correctly + if (this.get('state') === STATE_INIT) { + this._doneCallback && this._doneCallback(this); + } + + this.set('state', STATE_OK); + this.unset('error'); + }, + + error: function (callback) { + this._errorCallback = callback; + return this; + }, + + setError: function (error) { + // Invoke this._errorCallback if present, the first time + // the vis is instantiated and the're some errors + if (this.get('state') === STATE_INIT) { + this._errorCallback && this._errorCallback(error); + } + + this.set({ + state: STATE_ERROR, + error: error + }); + }, + + /** + * @return Array of {LayerModel} + */ + getLayers: function () { + return _.clone(this.map.layers.models); + }, + + /** + * @param {Integer} index Layer index (including base layer if present) + * @return {LayerModel} + */ + getLayer: function (index) { + return this.map.layers.at(index); + }, + + load: function (vizjson) { + var windshaftSettings = { + urlTemplate: vizjson.datasource.maps_api_template, + userName: vizjson.datasource.user_name, + client: vizjson.datasource.client, + apiKey: this.get('apiKey'), + authToken: this.get('authToken'), + templateName: vizjson.datasource.template_name + }; + + this._engine = this._createEngine(windshaftSettings); + + this._engine.on(Engine.Events.RELOAD_SUCCESS, this._onEngineReloadSuccess, this); + this._engine.on(Engine.Events.RELOAD_ERROR, this._onEngineReloadError, this); + this._engine.on(Engine.Events.LAYER_ERROR, this._onEngineLayerError, this); + + // Bind layerGroupModel object to engine + this.layerGroupModel = this._engine._cartoLayerGroup; + + // Create the public Analysis Service + this._analysisService = new AnalysisService({ + engine: this._engine, + apiKey: windshaftSettings.apiKey, + authToken: windshaftSettings.authToken + }); + + // Public wrapper exposing public methods. + this.analysis = { + analyse: this._analysisService.analyse.bind(this._analysisService), + findNodeById: this._analysisService.findNodeById.bind(this._analysisService) + }; + + var allowScrollInOptions = (vizjson.options && vizjson.options.scrollwheel) || vizjson.scrollwheel; + var allowDragging = util.isMobileDevice() || vizjson.hasZoomOverlay() || allowScrollInOptions; + + var renderMode = RenderModes.AUTO; + if (vizjson.vector === true) { + renderMode = RenderModes.VECTOR; + } else if (vizjson.vector === false) { + renderMode = RenderModes.RASTER; + } + + this.layersFactory = new LayersFactory({ engine: this._engine, windshaftSettings: windshaftSettings }); + + this.map = new Map({ + title: vizjson.title, + description: vizjson.description, + bounds: vizjson.bounds, + center: vizjson.center, + zoom: vizjson.zoom, + scrollwheel: !!allowScrollInOptions, + drag: allowDragging, + provider: vizjson.map_provider, + isFeatureInteractivityEnabled: this.get('interactiveFeatures'), + renderMode: renderMode + }, { + layersCollection: this._layersCollection, + layersFactory: this.layersFactory + }); + + this.listenTo(this.map, 'cartodbLayerMoved', this.reload); + + // Reset the collection of overlays + this.overlaysCollection.reset(vizjson.overlays); + + // Create the public Dataview Factory + // TODO: create dataviews more explicitly + this.dataviews = new DataviewsFactory({}, { + map: this.map, + engine: this._engine, + dataviewsCollection: this._dataviewsCollection + }); + + // Create layers + var analysisNodes = this._createAnalysisNodes(vizjson.analyses); + var layerModels = this._createLayers(vizjson.layers, analysisNodes); + this.map.layers.reset(layerModels); + + // Global variable for easier console debugging / testing + window.vis = this; + + _.defer(function () { + this.trigger('load', this); + }.bind(this)); + }, + + _createEngine: function (windshaftSettings) { + var engine = new Engine({ + apiKey: windshaftSettings.apiKey, + authToken: windshaftSettings.authToken, + username: windshaftSettings.userName, + serverUrl: windshaftSettings.urlTemplate, + templateName: windshaftSettings.templateName, + client: windshaftSettings.client + }); + + // TODO: Use engine.layerscollection in every reference + this._layersCollection = engine._layersCollection; + this._dataviewsCollection = engine._dataviewsCollection; + + return engine; + }, + + /** + * Return the engine for this visModel + */ + getEngine: function () { + return this._engine; + }, + + // we provide a method to set some new settings + setSettings: function (settings) { + this.settings.set(settings); + }, + + /** + * Check if an analysis is the source of any layer. + */ + _isAnalysisLinkedToLayer: function (analysisModel) { + return this._layersCollection.any(function (layerModel) { + return layerModel.hasSource(analysisModel); + }); + }, + + trackLoadingObject: function (object) { + if (this._loadingObjects.indexOf(object) === -1) { + this._loadingObjects.push(object); + } + this.set('loading', true); + }, + + untrackLoadingObject: function (object) { + var index = this._loadingObjects.indexOf(object); + if (index >= 0) { + this._loadingObjects.splice(index, 1); + if (this._loadingObjects.length === 0) { + this.set('loading', false); + } + } + }, + + /** + * Force a map instantiation. + * Only expected to be called once if {skipMapInstantiation} flag is set to true when vis is created. + */ + instantiateMap: function (options) { + options = options || {}; + if (this._instantiateMapWasCalled) { + return; + } + this._instantiateMapWasCalled = true; + + this.reload({ + success: function () { + this._onMapInstantiatedForTheFirstTime(); + options.success && options.success(); + }.bind(this), + error: function (error) { + options.error && options.error(error && error.message); + }, + includeFilters: false + }); + }, + + _onMapInstantiatedForTheFirstTime: function () { + var anyDataviewFiltered = this._isAnyDataviewFiltered(); + whenAllDataviewsFetched(this._dataviewsCollection, this._onDataviewFetched.bind(this)); + this._initBindsAfterFirstMapInstantiation(); + + anyDataviewFiltered && this.reload({ includeFilters: anyDataviewFiltered }); + }, + + _isAnyDataviewFiltered: function () { + return this._dataviewsCollection.isAnyDataviewFiltered(); + }, + + _onDataviewFetched: function () { + this.trigger('dataviewsFetched'); + }, + + _onEngineReloadSuccess: function () { + this.trigger('reloaded'); + var analysisNodes = AnalysisService.getUniqueAnalysisNodes(this._layersCollection, this._dataviewsCollection); + this._isAnyAnalysisNodeLoading(analysisNodes) ? this.trackLoadingObject(this) : this.untrackLoadingObject(this); + this.setOk(); + }, + + _onEngineReloadError: function (error) { + if (error && error.isGlobalError && error.isGlobalError()) { + this.setError(error); + } + }, + + _onEngineLayerError: function (error) { + if (error) { + this.map.trigger('error:' + error.type, error); + } + }, + + reload: function (options) { + if (this._instantiateMapWasCalled) { + this.trigger('reload'); + return this._engine.reload(options); + } + return Promise.resolve(); + }, + + _isAnyAnalysisNodeLoading: function (analysisNodes) { + return _.any(analysisNodes, function (analysisModel) { + return analysisModel.isLoading(); + }); + }, + + _initBindsAfterFirstMapInstantiation: function () { + this._layersCollection.bind('reset', this._onLayersResetted, this); + this._layersCollection.bind('add', this._onLayerAdded, this); + this._layersCollection.bind('remove', this._onLayerRemoved, this); + + if (this._dataviewsCollection) { + // When new dataviews are defined, a new instance of the map needs to be created + this._dataviewsCollection.on('add reset remove', _.debounce(this.invalidateSize, 10), this); + this.listenTo(this._dataviewsCollection, 'add', _.debounce(this._onDataviewAdded.bind(this), 10)); + this.listenTo(this._dataviewsCollection, 'remove', this._onDataviewRemoved); + } + }, + + _onDataviewRemoved: function (dataviewModel) { + if (dataviewModel.isFiltered()) { + this.reload({ + sourceId: dataviewModel.getSourceId() + }); + } + }, + + _onLayersResetted: function () { + this.reload(); + }, + + _onLayerAdded: function (layerModel, collection, opts) { + opts = opts || {}; + if (!opts.silent) { + this.reload({ + sourceId: layerModel.get('id') + }); + } + }, + + _onLayerRemoved: function (layerModel, collection, opts) { + opts = opts || {}; + if (!opts.silent) { + this.reload({ + sourceId: layerModel.get('id') + }); + } + }, + + _onDataviewAdded: function () { + this.reload(); + }, + + invalidateSize: function () { + this.trigger('invalidateSize'); + }, + + addCustomOverlay: function (overlayView) { + overlayView.type = 'custom'; + this.overlaysCollection.add(overlayView); + return overlayView; + }, + + isLoading: function () { + return this.get('loading'); + }, + + /** + * "Load" existing analyses from the viz.json. This will generate + * the analyses graphs and index analysis nodes in the + * collection of analysis + */ + _createAnalysisNodes: function (analysesDefinition) { + _.each(analysesDefinition, function (analysisDefinition) { + this._analysisService.analyse(analysisDefinition); + }, this); + }, + + _createLayers: function (layersDefinition, analysisNodes) { + var layers = _.map(layersDefinition, function (layerData, layerIndex) { + // Flatten "options" and set the "order" attribute + layerData = _.extend({}, + _.omit(layerData, 'options'), + layerData.options, { + order: layerIndex + } + ); + + if (layerData.source) { + layerData.source = this._analysisService.findNodeById(layerData.source); + } else { + // TODO: We'll be able to remove this (accepting sql option) once + // https://github.com/CartoDB/cartodb.js/issues/1754 is closed. + if (layerData.sql) { + layerData.source = this._analysisService.createAnalysisForLayer(layerData.id, layerData.sql); + delete layerData.sql; + } + } + return this.layersFactory.createLayer(layerData.type, layerData); + }, this); + return layers; + } +}); + +module.exports = VisModel; diff --git a/src/vis/vis/infowindow-template.js b/src/vis/vis/infowindow-template.js new file mode 100644 index 0000000..f6a051f --- /dev/null +++ b/src/vis/vis/infowindow-template.js @@ -0,0 +1,23 @@ +var INFOWINDOW_TEMPLATE = { + light: [ + '
', + 'x', + '
', + '
', + '{{#content.fields}}', + '{{#title}}

{{title}}

{{/title}}', + '{{#value}}', + '

{{{ value }}}

', + '{{/value}}', + '{{^value}}', + '

null

', + '{{/value}}', + '{{/content.fields}}', + '
', + '
', + '
', + '
' + ].join('') +}; + +module.exports = INFOWINDOW_TEMPLATE; diff --git a/src/windshaft-integration/legends/rule-to-bubble-legend-adapter.js b/src/windshaft-integration/legends/rule-to-bubble-legend-adapter.js new file mode 100644 index 0000000..7dc9a8e --- /dev/null +++ b/src/windshaft-integration/legends/rule-to-bubble-legend-adapter.js @@ -0,0 +1,38 @@ +var _ = require('underscore'); +var Rule = require('./rule'); + +var VALID_PROPS = ['marker-width', 'line-width']; + +var isEveryBucketValid = function (rule) { + var buckets = rule.getBucketsWithRangeFilter(); + return _.every(buckets, function (bucket) { + return bucket.filter.start != null && bucket.filter.end != null; + }); +}; + +var calculateValues = function (buckets) { + var lastBucket = _.last(buckets); + return _.chain(buckets) + .map('filter') + .map('start') + .concat(lastBucket.filter.end) + .value(); +}; + +module.exports = { + canAdapt: function (rule) { + rule = new Rule(rule); + return rule.matchesAnyProperty(VALID_PROPS) && isEveryBucketValid(rule); + }, + + adapt: function (rules) { + var rule = new Rule(rules[0]); + var buckets = rule.getBucketsWithRangeFilter(); + + return { + values: calculateValues(buckets), + sizes: _.map(buckets, 'value'), + avg: rule.getFilterAvg() + }; + } +}; diff --git a/src/windshaft-integration/legends/rule-to-category-legend-adapter.js b/src/windshaft-integration/legends/rule-to-category-legend-adapter.js new file mode 100644 index 0000000..36e82ff --- /dev/null +++ b/src/windshaft-integration/legends/rule-to-category-legend-adapter.js @@ -0,0 +1,62 @@ +var _ = require('underscore'); +var Rule = require('./rule'); + +var VALID_PROPS = ['line-color', 'marker-fill', 'polygon-fill', 'marker-file']; +var VALID_MAPPINGS = ['=']; + +var isEveryBucketValid = function (rule) { + var buckets = rule.getBucketsWithCategoryFilter(); + return _.every(buckets, function (bucket) { + return bucket.filter.name != null && bucket.value != null; + }); +}; + +var generateCategories = function (bucketsColor, bucketsIcon) { + return _.map(bucketsColor, function (bucketColor) { + var bucketIcon = _.find(bucketsIcon, function (bucket) { + return bucket.filter.name === bucketColor.filter.name; + }); + return { + title: bucketColor.filter.name, + icon: (bucketIcon && bucketIcon.value) ? _extractURL(bucketIcon.value) : '', + color: bucketColor.value + }; + }); +}; + +var _extractURL = function (str) { + var url = ''; + var pattern = /(http|https):\/\/\S+\.(?:gif|jpeg|jpg|png|webp|svg)/g; + var match = str.match(pattern); + if (match) { + url = match[0]; + } + return url; +}; + +module.exports = { + canAdapt: function (rule) { + rule = new Rule(rule); + return rule.matchesAnyProperty(VALID_PROPS) && + rule.matchesAnyMapping(VALID_MAPPINGS) && + isEveryBucketValid(rule); + }, + + adapt: function (rules) { + var ruleColor = new Rule(rules[0]); + var ruleIcon = new Rule(rules[1]); + + var categoryBucketsColor = ruleColor.getBucketsWithCategoryFilter(); + var categoryBucketsIcon = ruleIcon.getBucketsWithCategoryFilter(); + var defaultBucketsColor = ruleColor.getBucketsWithDefaultFilter(); + var defaultBucketsIcon = ruleIcon.getBucketsWithDefaultFilter(); + + return { + categories: generateCategories(categoryBucketsColor, categoryBucketsIcon), + default: { + icon: _.isEmpty(defaultBucketsIcon) ? '' : _extractURL(defaultBucketsIcon[0].value), + color: _.isEmpty(defaultBucketsColor) ? '' : defaultBucketsColor[0].value + } + }; + } +}; diff --git a/src/windshaft-integration/legends/rule-to-choropleth-legend-adapter.js b/src/windshaft-integration/legends/rule-to-choropleth-legend-adapter.js new file mode 100644 index 0000000..f9e50dc --- /dev/null +++ b/src/windshaft-integration/legends/rule-to-choropleth-legend-adapter.js @@ -0,0 +1,54 @@ +var _ = require('underscore'); +var Rule = require('./rule'); + +var VALID_PROPS = ['line-color', 'marker-fill', 'polygon-fill']; +var VALID_MAPPINGS = ['>', '>=', '<', '<=']; + +var isEveryBucketValid = function (rule) { + var buckets = rule.getBucketsWithRangeFilter(); + return _.every(buckets, function (bucket) { + return bucket.filter.start != null && bucket.filter.end != null; + }); +}; + +var generateColors = function (buckets) { + if (buckets.length === 1) { + var bucket = buckets[0]; + var labelStart = bucket.filter.start; + var labelEnd = bucket.filter.end; + return [{ value: bucket.value, label: labelStart.toString() }, { value: bucket.value, label: labelEnd.toString() }]; + } + return _.map(buckets, function (bucket, i) { + var label = ''; + if (i === 0) { + label = bucket.filter.start; + } else if (i === buckets.length - 1) { + label = bucket.filter.end; + } + return { value: bucket.value, label: label.toString() }; + }); +}; + +module.exports = { + canAdapt: function (rule) { + rule = new Rule(rule); + return rule.matchesAnyProperty(VALID_PROPS) && + rule.matchesAnyMapping(VALID_MAPPINGS) && + isEveryBucketValid(rule); + }, + + adapt: function (rules) { + var rule = new Rule(rules[0]); + + var rangeBuckets = rule.getBucketsWithRangeFilter(); + var lastBucket = _.last(rangeBuckets); + var firstBucket = _.first(rangeBuckets); + + return { + colors: generateColors(rangeBuckets), + avg: rule.getFilterAvg(), + max: lastBucket.filter.end, + min: firstBucket.filter.start + }; + } +}; diff --git a/src/windshaft-integration/legends/rule-to-legend-model-adapters.js b/src/windshaft-integration/legends/rule-to-legend-model-adapters.js new file mode 100644 index 0000000..1423d70 --- /dev/null +++ b/src/windshaft-integration/legends/rule-to-legend-model-adapters.js @@ -0,0 +1,11 @@ +var ADAPTERS = { + bubble: require('./rule-to-bubble-legend-adapter'), + choropleth: require('./rule-to-choropleth-legend-adapter'), + category: require('./rule-to-category-legend-adapter') +}; + +module.exports = { + getAdapterForLegend: function (legendModel) { + return ADAPTERS[legendModel.get('type')]; + } +}; diff --git a/src/windshaft-integration/legends/rule.js b/src/windshaft-integration/legends/rule.js new file mode 100644 index 0000000..5a3cdb1 --- /dev/null +++ b/src/windshaft-integration/legends/rule.js @@ -0,0 +1,66 @@ +var _ = require('underscore'); + +var PROPERTY_KEY = 'prop'; +var MAPPING_KEY = 'mapping'; + +var RANGE_FILTER_TYPE = 'range'; +var CATEGORY_FILTER_TYPE = 'category'; +var DEFAULT_FILTER_TYPE = 'default'; + +var Rule = function (rule) { + this._rule = rule; +}; + +Rule.prototype.matchesAnyProperty = function (props) { + return this._matches(this._rule[PROPERTY_KEY], props); +}; + +Rule.prototype.matchesAnyMapping = function (mappings) { + return this._matches(this._rule[MAPPING_KEY], mappings); +}; + +Rule.prototype._matches = function (value, acceptedValues) { + if (_.isString(acceptedValues)) { + acceptedValues = [ acceptedValues ]; + } + return _.contains(acceptedValues, value); +}; + +Rule.prototype.getBucketsWithRangeFilter = function () { + return this._getBucketsByFilterType(RANGE_FILTER_TYPE); +}; + +Rule.prototype.getBucketsWithCategoryFilter = function () { + return this._getBucketsByFilterType(CATEGORY_FILTER_TYPE); +}; + +Rule.prototype.getBucketsWithDefaultFilter = function () { + return this._getBucketsByFilterType(DEFAULT_FILTER_TYPE); +}; + +Rule.prototype._getBucketsByFilterType = function (filterType) { + if (this._rule) { + return _.select(this._rule.buckets, function (bucket) { + return bucket.filter.type === filterType; + }); + } + return []; +}; + +Rule.prototype.getColumn = function () { + return this._rule.column; +}; + +Rule.prototype.getMapping = function () { + return this._rule.mapping; +}; + +Rule.prototype.getProperty = function () { + return this._rule.prop; +}; + +Rule.prototype.getFilterAvg = function () { + return this._rule.stats.filter_avg; +}; + +module.exports = Rule; diff --git a/src/windshaft-integration/model-updater.js b/src/windshaft-integration/model-updater.js new file mode 100644 index 0000000..39a5ede --- /dev/null +++ b/src/windshaft-integration/model-updater.js @@ -0,0 +1,266 @@ +var _ = require('underscore'); +var log = require('../cdb.log'); +var util = require('../core/util.js'); +var RuleToLegendModelAdapters = require('./legends/rule-to-legend-model-adapters'); +var AnalysisService = require('../analysis/analysis-service'); +var Backbone = require('backbone'); + +function getSubdomain (subdomains, resource) { + var index = util.crc32(resource) % subdomains.length; + return subdomains[index]; +} + +/** + * This class exposes a method that knows how to set/update the metadata on internal + * CartoDB.js models that are linked to a "resource" in the Maps API. + */ +var ModelUpdater = function (deps) { + if (!deps.layerGroupModel) throw new Error('layerGroupModel is required'); + if (!deps.layersCollection) throw new Error('layersCollection is required'); + if (!deps.dataviewsCollection) throw new Error('dataviewsCollection is required'); + + this._layerGroupModel = deps.layerGroupModel; + this._layersCollection = deps.layersCollection; + this._dataviewsCollection = deps.dataviewsCollection; +}; + +ModelUpdater.prototype.updateModels = function (responseWrapper, sourceId, forceFetch) { + this._updateLayerModels(responseWrapper); + this._updateLayerGroupModel(responseWrapper); + this._updateDataviewModels(responseWrapper, sourceId, forceFetch); + this._updateAnalysisModels(responseWrapper); +}; + +ModelUpdater.prototype._updateLayerGroupModel = function (responseWrapper) { + var urls = { + tiles: this._generateTileURLTemplate(responseWrapper), + subdomains: responseWrapper.getSupportedSubdomains(), + grids: this._calculateGridURLTemplatesForCartoDBLayers(responseWrapper), + attributes: this._calculateAttributesBaseURLsForCartoDBLayers(responseWrapper), + image: this._calculateStaticMapURL(responseWrapper) + }; + + this._layerGroupModel.set({ + indexOfLayersInWindshaft: responseWrapper.getLayerIndexesByType('mapnik'), + urls: urls + }); +}; + +ModelUpdater.prototype._calculateStaticMapURL = function (responseWrapper) { + return [ + responseWrapper.getStaticBaseURL(), + '{z}/{lat}/{lng}/{width}/{height}.{format}' + ].join('/'); +}; + +ModelUpdater.prototype._generateTileURLTemplate = function (responseWrapper) { + return responseWrapper.getBaseURL() + '/{layerIndexes}/{z}/{x}/{y}.{format}'; +}; + +ModelUpdater.prototype._calculateGridURLTemplatesForCartoDBLayers = function (responseWrapper) { + var urlTemplates = []; + var indexesOfMapnikLayers = responseWrapper.getLayerIndexesByType('mapnik'); + if (indexesOfMapnikLayers.length > 0) { + _.each(indexesOfMapnikLayers, function (index) { + var layerUrlTemplates = []; + var gridURLTemplate = this._generateGridURLTemplate(responseWrapper, index); + var subdomains = responseWrapper.getSupportedSubdomains(); + if (subdomains.length) { + _.each(subdomains, function (subdomain) { + layerUrlTemplates.push(gridURLTemplate.replace('{s}', subdomain)); + }); + } else { + layerUrlTemplates.push(gridURLTemplate); + } + + urlTemplates.push(layerUrlTemplates); + }, this); + } + return urlTemplates; +}; + +ModelUpdater.prototype._generateGridURLTemplate = function (responseWrapper, index) { + return responseWrapper.getBaseURL() + '/' + index + '/{z}/{x}/{y}.grid.json'; +}; + +ModelUpdater.prototype._calculateAttributesBaseURLsForCartoDBLayers = function (responseWrapper) { + var urls = []; + var indexesOfMapnikLayers = responseWrapper.getLayerIndexesByType('mapnik'); + if (indexesOfMapnikLayers.length > 0) { + _.each(indexesOfMapnikLayers, function (index) { + urls.push(this._generateAttributesBaseURL(responseWrapper, index)); + }, this); + } + return urls; +}; + +ModelUpdater.prototype._generateAttributesBaseURL = function (responseWrapper, index) { + var baseURL = responseWrapper.getBaseURL() + '/' + index + '/attributes'; + if (baseURL.indexOf('{s}') >= 0) { + var subdomain = getSubdomain(responseWrapper.getSupportedSubdomains(), baseURL); + baseURL = baseURL.replace('{s}', subdomain); + } + return baseURL; +}; + +ModelUpdater.prototype._updateLayerModels = function (responseWrapper) { + // CartoDB / mapnik layers + var indexesOfMapnikLayers = responseWrapper.getLayerIndexesByType('mapnik'); + _.each(this._layersCollection.getCartoDBLayers(), function (layerModel, localLayerIndex) { + var responseWrapperLayerIndex = indexesOfMapnikLayers[localLayerIndex]; + layerModel.set('meta', responseWrapper.getLayerMetadata(responseWrapperLayerIndex)); + this._updateLegendModels(layerModel, responseWrapperLayerIndex, responseWrapper); + + layerModel.setOk(); + }, this); + + // Torque / torque layers + var indexesOfTorqueLayers = responseWrapper.getLayerIndexesByType('torque'); + _.each(this._layersCollection.getTorqueLayers(), function (layerModel, localLayerIndex) { + var responseWrapperLayerIndex = indexesOfTorqueLayers[localLayerIndex]; + var meta = responseWrapper.getLayerMetadata(responseWrapperLayerIndex); + layerModel.set('meta', meta); + // deep-insight.js expects meta attributes as attributes for some reason + layerModel.set(meta); + layerModel.set('subdomains', responseWrapper.getSupportedSubdomains()); + layerModel.set('tileURLTemplates', this._calculateTileURLTemplatesForTorqueLayers(responseWrapper)); + this._updateLegendModels(layerModel, responseWrapperLayerIndex, responseWrapper); + + layerModel.setOk(); + }, this); +}; + +ModelUpdater.prototype._calculateTileURLTemplatesForTorqueLayers = function (responseWrapper) { + var urlTemplates = []; + var indexesOfTorqueLayers = responseWrapper.getLayerIndexesByType('torque'); + if (indexesOfTorqueLayers.length > 0) { + urlTemplates.push(this._generateTorqueTileURLTemplate(responseWrapper, indexesOfTorqueLayers)); + } + return urlTemplates; +}; + +ModelUpdater.prototype._generateTorqueTileURLTemplate = function (responseWrapper, layerIndexes) { + return responseWrapper.getBaseURL() + '/' + layerIndexes.join(',') + '/{z}/{x}/{y}.json.torque'; +}; + +ModelUpdater.prototype._updateLegendModels = function (layerModel, remoteLayerIndex, responseWrapper) { + var layerMetadata = responseWrapper.getLayerMetadata(remoteLayerIndex); + _.each(this._getLayerLegends(layerModel), function (legendModel) { + this._updateLegendModel(legendModel, layerMetadata); + }, this); +}; + +ModelUpdater.prototype._updateLegendModel = function (legendModel, layerMetadata) { + var cartoCSSRules = layerMetadata && layerMetadata.cartocss_meta && layerMetadata.cartocss_meta.rules; + try { + var newLegendAttrs = { + state: 'success' + }; + if (cartoCSSRules) { + var adapter = RuleToLegendModelAdapters.getAdapterForLegend(legendModel); + var rulesForLegend = _.filter(cartoCSSRules, adapter.canAdapt); + if (!_.isEmpty(rulesForLegend)) { + newLegendAttrs = _.extend(newLegendAttrs, adapter.adapt(rulesForLegend)); + } + } + legendModel.set(newLegendAttrs); + } catch (error) { + legendModel.set({ state: 'error' }); + log.error("legend of type '" + legendModel.get('type') + "' couldn't be updated: " + error.message); + } +}; + +ModelUpdater.prototype._updateDataviewModels = function (responseWrapper, sourceId, forceFetch) { + this._dataviewsCollection.each(function (dataviewModel) { + var dataviewMetadata = responseWrapper.getDataviewMetadata(dataviewModel.get('id')); + if (dataviewMetadata) { + dataviewModel.set({ + url: dataviewMetadata.url[this._getProtocol()] + }, { + sourceId: sourceId, + forceFetch: forceFetch + }); + } + }, this); +}; + +ModelUpdater.prototype._updateAnalysisModels = function (responseWrapper) { + var analysisNodesCollection = this._getUniqueAnalysisNodesCollection(); + analysisNodesCollection.each(function (analysisNode) { + var analysisMetadata = responseWrapper.getAnalysisNodeMetadata(analysisNode.get('id')); + var attrs; + if (analysisMetadata) { + attrs = { + status: analysisMetadata.status, + url: analysisMetadata.url[this._getProtocol()], + query: analysisMetadata.query + }; + + attrs = _.omit(attrs, analysisNode.getParamNames()); + + if (analysisMetadata.error_message) { + attrs = _.extend(attrs, { + error: { + message: analysisMetadata.error_message + } + }); + analysisNode.set(attrs); + } else { + analysisNode.set(attrs); + analysisNode.setOk(); + } + } + }, this); +}; + +ModelUpdater.prototype._getProtocol = function () { + // When running tests window.locationn.protocol using the jasmine test runner, + // window.location.protocol returns 'file:'. This is a little hack to make tests happy. + if (window.location.protocol === 'file:') { + return 'http'; + } + return window.location.protocol.replace(':', ''); +}; + +ModelUpdater.prototype.setErrors = function (errors) { + _.each(errors, this._setError, this); + this._setLegendErrors(); +}; + +ModelUpdater.prototype._setError = function (error) { + if (error.isLayerError()) { + var layerModel = this._layersCollection.get(error.layerId); + layerModel && layerModel.setError(error); + } else if (error.isAnalysisError()) { + var analysisNodesCollection = this._getUniqueAnalysisNodesCollection(); + var analysisModel = analysisNodesCollection.get(error.analysisId); + analysisModel && analysisModel.setError(error); + } +}; + +ModelUpdater.prototype._setLegendErrors = function () { + var legendModels = this._layersCollection.chain() + .map(this._getLayerLegends) + .compact() + .flatten() + .value(); + + _.each(legendModels, function (legendModel) { + legendModel.set('state', 'error'); + }); +}; + +ModelUpdater.prototype._getLayerLegends = function (layerModel) { + return layerModel.legends && [ + layerModel.legends.bubble, + layerModel.legends.category, + layerModel.legends.choropleth + ]; +}; + +ModelUpdater.prototype._getUniqueAnalysisNodesCollection = function () { + var analysisNodes = AnalysisService.getUniqueAnalysisNodes(this._layersCollection, this._dataviewsCollection); + return new Backbone.Collection(analysisNodes); +}; + +module.exports = ModelUpdater; diff --git a/src/windshaft/client.js b/src/windshaft/client.js new file mode 100644 index 0000000..14c4e02 --- /dev/null +++ b/src/windshaft/client.js @@ -0,0 +1,184 @@ +var $ = require('jquery'); +var _ = require('underscore'); +var LZMA = require('../../vendor/lzma'); +var util = require('../core/util'); +var WindshaftConfig = require('./config'); +var RequestTracker = require('./request-tracker'); +var log = require('../cdb.log'); +var parseWindshaftErrors = require('./error-parser'); + +var validatePresenceOfOptions = function (options, requiredOptions) { + var missingOptions = _.filter(requiredOptions, function (option) { + return !options[option]; + }); + if (missingOptions.length) { + throw new Error('WindshaftClient could not be initialized. The following options are missing: ' + missingOptions.join(', ')); + } +}; + +var MAX_URL_LENGTH = 2033; +var COMPRESSION_LEVEL = 3; +/* The max number of times the same map can be instantiated */ +var MAP_INSTANTIATION_LIMIT = 3; + +/** + * Windshaft client. It provides a method to create instances of maps in Windshaft. + * @param {object} options Options to set up the client + */ +var WindshaftClient = function (settings) { + validatePresenceOfOptions(settings, ['urlTemplate', 'userName']); + + if (settings.templateName) { + this.endpoints = { + get: [WindshaftConfig.MAPS_API_BASE_URL, 'named', settings.templateName, 'jsonp'].join('/'), + post: [WindshaftConfig.MAPS_API_BASE_URL, 'named', settings.templateName].join('/') + }; + } else { + this.endpoints = { + get: WindshaftConfig.MAPS_API_BASE_URL, + post: WindshaftConfig.MAPS_API_BASE_URL + }; + } + + this.url = settings.urlTemplate.replace('{user}', settings.userName); + this._requestTracker = new RequestTracker(MAP_INSTANTIATION_LIMIT); +}; + +WindshaftClient.prototype.instantiateMap = function (request) { + // TODO: update options, use promises or explicit callbacks function (error, params). + if (this._requestTracker.canRequestBePerformed(request)) { + this._performRequest(request, { + success: function (response) { + this._requestTracker.track(request, response); + if (response.errors) { + var parsedErrors = parseWindshaftErrors(response); + request.options.error && request.options.error(parsedErrors); + } else { + request.options.success && request.options.success(response); + } + }.bind(this), + error: function (xhr, textStatus) { + // Ignore error if request was explicitly aborted + if (textStatus === 'abort') return; + var errors = {}; + var parsedErrors = {}; + try { + errors = JSON.parse(xhr.responseText); + parsedErrors = parseWindshaftErrors(errors); + } catch (e) { } + this._requestTracker.track(request, errors); + request.options.error && request.options.error(parsedErrors); + }.bind(this) + }); + } else { + log.error('Maximum number of subsequent equal requests to the Maps API reached (' + MAP_INSTANTIATION_LIMIT + '):', request.payload, request.params); + request.options.error && request.options.error({}); + } +}; + +WindshaftClient.prototype._performRequest = function (request, ajaxOptions) { + var mapDefinition = request.payload; + var params = request.params; + + var encodedURL = this._generateEncodedURL(mapDefinition, params); + if (this._isURLValid(encodedURL)) { + this._get(encodedURL, ajaxOptions); + } else { + this._generateCompressedURL(mapDefinition, params, function (compressedURL) { + if (this._isURLValid(compressedURL)) { + this._get(compressedURL, ajaxOptions); + } else { + var url = this._getURL(params, 'post'); + this._post(url, mapDefinition, ajaxOptions); + } + }.bind(this)); + } +}; + +WindshaftClient.prototype._generateEncodedURL = function (payload, params) { + params = _.extend({ + config: JSON.stringify(payload) + }, params); + + return this._getURL(params); +}; + +WindshaftClient.prototype._generateCompressedURL = function (payload, params, callback) { + var data = JSON.stringify({ + config: JSON.stringify(payload) + }); + + LZMA.compress(data, COMPRESSION_LEVEL, function (compressedPayload) { + params = _.extend({ + lzma: util.array2hex(compressedPayload) + }, params); + + callback(this._getURL(params)); + }.bind(this)); +}; + +WindshaftClient.prototype._isURLValid = function (url) { + return url.length < MAX_URL_LENGTH; +}; + +WindshaftClient.prototype._post = function (url, payload, options) { + this._abortPreviousRequest(); + this._previousRequest = $.ajax({ + url: url, + crossOrigin: true, + method: 'POST', + dataType: 'json', + contentType: 'application/json', + data: JSON.stringify(payload), + success: options.success, + error: options.error + }); +}; + +WindshaftClient.prototype._get = function (url, options) { + this._abortPreviousRequest(); + this._previousRequest = $.ajax({ + url: url, + method: 'GET', + dataType: 'jsonp', + jsonpCallback: this._jsonpCallbackName(url), + cache: true, + success: options.success, + error: options.error + }); +}; + +WindshaftClient.prototype._abortPreviousRequest = function () { + if (this._previousRequest) { + this._previousRequest.abort(); + } +}; + +WindshaftClient.prototype._getURL = function (params, method) { + method = method || 'get'; + var queryString = this._convertParamsToQueryString(params); + var endpoint = this.endpoints[method]; + return [this.url, endpoint].join('/') + queryString; +}; + +WindshaftClient.prototype._convertParamsToQueryString = function (params) { + var queryString = []; + _.each(params, function (value, name) { + if (value instanceof Array) { + _.each(value, function (oneValue) { + queryString.push(name + '[]=' + oneValue); + }); + } else if (value instanceof Object) { + queryString.push(name + '=' + encodeURIComponent(JSON.stringify(value))); + } else { + queryString.push(name + '=' + encodeURIComponent(value)); + } + }); + return queryString.length > 0 ? '?' + queryString.join('&') : ''; +}; + +WindshaftClient.prototype._jsonpCallbackName = function (payload) { + return '_cdbc_' + util.uniqueCallbackName(payload); +}; + +module.exports = WindshaftClient; diff --git a/src/windshaft/config.js b/src/windshaft/config.js new file mode 100644 index 0000000..5412240 --- /dev/null +++ b/src/windshaft/config.js @@ -0,0 +1,3 @@ +module.exports = { + MAPS_API_BASE_URL: 'api/v1/map' +}; diff --git a/src/windshaft/error-parser.js b/src/windshaft/error-parser.js new file mode 100644 index 0000000..2283ed5 --- /dev/null +++ b/src/windshaft/error-parser.js @@ -0,0 +1,31 @@ +var _ = require('underscore'); +var WindshaftError = require('./error'); + +var parseWindshaftErrors = function (response, type) { + response = response || {}; + if (response.responseJSON) { + response = response.responseJSON; + } + if (response.errors_with_context) { + return _.map(response.errors_with_context, function (error) { + return new WindshaftError(error, type); + }); + } + if (response.errors) { + var content = typeof response.errors[0] === 'string' + ? { message: response.errors[0] } + : response.errors[0]; + + return [ + new WindshaftError(content, type) + ]; + } + if (response.statusText) { + return [ + new WindshaftError({ message: response.statusText }, type, 'ajax') + ]; + } + return []; +}; + +module.exports = parseWindshaftErrors; diff --git a/src/windshaft/error.js b/src/windshaft/error.js new file mode 100644 index 0000000..414997a --- /dev/null +++ b/src/windshaft/error.js @@ -0,0 +1,61 @@ +var WINDSHAFT_ERRORS = require('../constants').WINDSHAFT_ERRORS; + +var WindshaftError = function (error, type, origin) { + this._error = error; + + this.origin = origin || 'windshaft'; + this.type = getType(error.type, type, WINDSHAFT_ERRORS.GENERIC); + this.subtype = error.subtype; + this.message = truncateMessage(error.message); + this.context = error.context; + + if (this.isLayerError(error.type)) { + this.context = error.layer && error.layer.context; + this.layerId = error.layer && error.layer.id; + } + + if (this.isAnalysisError(error.type) && error.analysis) { + this.context = error.analysis.context; + this.analysisId = error.analysis.node_id || error.analysis.id; + } +}; + +WindshaftError.prototype.isGlobalError = function (errorType) { + return !this.isLayerError(errorType) && !this.isAnalysisError(errorType); +}; + +WindshaftError.prototype.isLayerError = function (errorType) { + errorType = errorType || this._error.type; + return errorType === WINDSHAFT_ERRORS.LAYER; +}; + +WindshaftError.prototype.isAnalysisError = function (errorType) { + errorType = errorType || this._error.type; + return errorType === WINDSHAFT_ERRORS.ANALYSIS; +}; + +// Helper functions + +function truncateMessage (message) { + var MAX_SIZE = 256; + + return message && message.length > MAX_SIZE + ? message.substring(0, MAX_SIZE) + : message; +} + +function getType (originalType, forcedType, genericType) { + if (!originalType || originalType === WINDSHAFT_ERRORS.UNKNOWN) { + if (forcedType) { + return forcedType; + } else if (originalType !== WINDSHAFT_ERRORS.UNKNOWN) { + return genericType; + } else { + return WINDSHAFT_ERRORS.UNKNOWN; + } + } else { + return originalType; + } +} + +module.exports = WindshaftError; diff --git a/src/windshaft/filters/base.js b/src/windshaft/filters/base.js new file mode 100644 index 0000000..6615d7d --- /dev/null +++ b/src/windshaft/filters/base.js @@ -0,0 +1,17 @@ +var Model = require('../../core/model'); + +module.exports = Model.extend({ + + isEmpty: function () { + throw new Error('Filters must implement the .isEmpty method'); + }, + + toJSON: function () { + throw new Error('Filters must implement the .toJSON method'); + }, + + remove: function () { + this.trigger('destroy', this); + this.stopListening(); + } +}); diff --git a/src/windshaft/filters/bounding-box.js b/src/windshaft/filters/bounding-box.js new file mode 100644 index 0000000..8a7ad8d --- /dev/null +++ b/src/windshaft/filters/bounding-box.js @@ -0,0 +1,55 @@ +var _ = require('underscore'); +var Model = require('../../core/model'); +var BOUNDING_BOX_FILTER_WAIT = 350; + +module.exports = Model.extend({ + initialize: function (mapAdapter) { + this._bounds = {}; + + if (mapAdapter) { + this._mapAdapter = mapAdapter; + this.setBounds(this._mapAdapter.getBounds()); + this._initBinds(); + } + }, + + _initBinds: function () { + this.listenTo(this._mapAdapter, 'boundsChanged', _.debounce(this._boundsChanged, BOUNDING_BOX_FILTER_WAIT)); + }, + + _stopBinds: function () { + if (this._mapAdapter) { + this.stopListening(this._mapAdapter, 'boundsChanged'); + } + }, + + _boundsChanged: function (bounds) { + this.setBounds(bounds); + }, + + setBounds: function (bounds) { + this._bounds = bounds; + this.trigger('boundsChanged', bounds); + }, + + getBounds: function () { + return this._bounds; + }, + + areBoundsAvailable: function () { + return _.isFinite(this._bounds.west); + }, + + serialize: function () { + return [ + this._bounds.west, + this._bounds.south, + this._bounds.east, + this._bounds.north + ].join(','); + }, + + clean: function () { + this._stopBinds(); + } +}); diff --git a/src/windshaft/filters/category.js b/src/windshaft/filters/category.js new file mode 100644 index 0000000..62b18d2 --- /dev/null +++ b/src/windshaft/filters/category.js @@ -0,0 +1,144 @@ +var _ = require('underscore'); +var Backbone = require('backbone'); +var WindshaftFilterBase = require('./base'); + +/** + * Filter used by the category dataview + */ +module.exports = WindshaftFilterBase.extend({ + + defaults: { + rejectAll: false + }, + + initialize: function () { + this.rejectedCategories = new Backbone.Collection(); + this.acceptedCategories = new Backbone.Collection(); + this._initBinds(); + }, + + _initBinds: function () { + this.listenTo(this.rejectedCategories, 'add remove', function () { + this.set('rejectAll', false); + }); + this.listenTo(this.acceptedCategories, 'add remove', function () { + this.set('rejectAll', false); + }); + }, + + isEmpty: function () { + return this.rejectedCategories.size() === 0 && this.acceptedCategories.size() === 0 && !this.get('rejectAll'); + }, + + accept: function (values, applyFilter) { + values = !_.isArray(values) ? [values] : values; + + _.each(values, function (value) { + var d = { name: value }; + var rejectedMdls = this.rejectedCategories.where(d); + var acceptedMdls = this.acceptedCategories.where(d); + if (rejectedMdls.length > 0) { + this.rejectedCategories.remove(rejectedMdls); + } else if (!acceptedMdls.length) { + this.acceptedCategories.add(d); + } + }, this); + + if (applyFilter !== false) { + this.applyFilter(); + } + }, + + acceptAll: function () { + this.set('rejectAll', false); + this.cleanFilter(); + }, + + isAccepted: function (name) { + return this.acceptedCategories.where({ name: name }).length > 0; + }, + + reject: function (values, applyFilter) { + values = !_.isArray(values) ? [values] : values; + + _.each(values, function (value) { + var d = { name: value }; + var acceptedMdls = this.acceptedCategories.where(d); + var rejectedMdls = this.rejectedCategories.where(d); + if (acceptedMdls.length > 0) { + this.acceptedCategories.remove(acceptedMdls); + } else { + if (!rejectedMdls.length) { + this.rejectedCategories.add(d); + } + } + }, this); + + if (applyFilter !== false) { + this.applyFilter(); + } + }, + + isRejected: function (name) { + var acceptCount = this.acceptedCategories.size(); + if (this.rejectedCategories.where({ name: name }).length > 0) { + return true; + } else if (acceptCount > 0 && this.acceptedCategories.where({ name: name }).length === 0) { + return true; + } else if (this.get('rejectAll')) { + return true; + } else { + return false; + } + }, + + rejectAll: function () { + this.set('rejectAll', true); + this.cleanFilter(); + }, + + cleanFilter: function (triggerChange) { + this.acceptedCategories.reset(); + this.rejectedCategories.reset(); + if (triggerChange !== false) { + this.applyFilter(); + } + }, + + applyFilter: function () { + this.trigger('change', this); + }, + + areAllRejected: function () { + return this.get('rejectAll'); + }, + + toJSON: function () { + var filter = {}; + var rejectCount = this.rejectedCategories.size(); + var acceptCount = this.acceptedCategories.size(); + var acceptedCats = { accept: _.pluck(this.acceptedCategories.toJSON(), 'name') }; + var rejectedCats = { reject: _.pluck(this.rejectedCategories.toJSON(), 'name') }; + + if (this.get('rejectAll')) { + filter = { accept: [] }; + } else if (acceptCount > 0) { + filter = acceptedCats; + } else if (rejectCount > 0 && acceptCount === 0) { + filter = rejectedCats; + } + + var json = {}; + json[this.get('dataviewId')] = filter; + + return json; + }, + + getAcceptedCategoryNames: function () { + return this.acceptedCategories.map(function (category) { return category.get('name'); }); + }, + + getRejectedCategoryNames: function () { + return this.rejectedCategories.map(function (category) { return category.get('name'); }); + } +}); diff --git a/src/windshaft/filters/circle.js b/src/windshaft/filters/circle.js new file mode 100644 index 0000000..cb6afd4 --- /dev/null +++ b/src/windshaft/filters/circle.js @@ -0,0 +1,20 @@ +var Model = require('../../core/model'); + +module.exports = Model.extend({ + initialize: function () { + this._circle = {}; + }, + + setCircle: function (circle) { + this._circle = circle; + this.trigger('circleChanged', circle); + }, + + getCircle: function () { + return this._circle; + }, + + serialize: function () { + return encodeURIComponent(JSON.stringify(this.getCircle())); + } +}); diff --git a/src/windshaft/filters/polygon.js b/src/windshaft/filters/polygon.js new file mode 100644 index 0000000..51f6d0e --- /dev/null +++ b/src/windshaft/filters/polygon.js @@ -0,0 +1,20 @@ +var Model = require('../../core/model'); + +module.exports = Model.extend({ + initialize: function () { + this._polygon = {}; + }, + + setPolygon: function (polygon) { + this._polygon = polygon; + this.trigger('polygonChanged', polygon); + }, + + getPolygon: function () { + return this._polygon; + }, + + serialize: function () { + return encodeURIComponent(JSON.stringify(this.getPolygon())); + } +}); diff --git a/src/windshaft/filters/range.js b/src/windshaft/filters/range.js new file mode 100644 index 0000000..7676fa1 --- /dev/null +++ b/src/windshaft/filters/range.js @@ -0,0 +1,31 @@ +var _ = require('underscore'); +var WindshaftFilterBase = require('./base'); + +module.exports = WindshaftFilterBase.extend({ + + isEmpty: function () { + return _.isUndefined(this.get('min')) && _.isUndefined(this.get('max')); + }, + + setRange: function (min, max) { + this.set({ + min: min, + max: max + }); + }, + + unsetRange: function () { + this.setRange(undefined, undefined); + }, + + toJSON: function () { + var json = {}; + json[this.get('dataviewId')] = { + min: this.get('min'), + max: this.get('max'), + column_type: this.get('column_type') + }; + + return json; + } +}); diff --git a/src/windshaft/map-serializer/anonymous-map-serializer/analysis-serializer.js b/src/windshaft/map-serializer/anonymous-map-serializer/analysis-serializer.js new file mode 100644 index 0000000..2612bf2 --- /dev/null +++ b/src/windshaft/map-serializer/anonymous-map-serializer/analysis-serializer.js @@ -0,0 +1,42 @@ +var _ = require('underscore'); +var AnalysisService = require('../../../analysis/analysis-service'); + +/** + * Return a payload with the serialization of all the analyses in the + * layersCollection and the dataviewsCollection. + */ +function serialize (layersCollection, dataviewsCollection) { + var analysisList = AnalysisService.getAnalysisList(layersCollection, dataviewsCollection); + return _generateUniqueAnalysisList(analysisList); +} + +/** + * Return an analysis list without duplicated or nested analyses + */ +function _generateUniqueAnalysisList (analysisList) { + var analysisIds = {}; + return _.reduce(analysisList, function (list, analysis) { + if (!analysisIds[analysis.get('id')] && !_isAnalysisPartOfOtherAnalyses(analysis, analysisList)) { + analysisIds[analysis.get('id')] = true; // keep a set of already added analysis. + list.push(analysis.toJSON()); + } + return list; + }, []); +} + +/** + * Check if an analysis is referenced by other anylisis in the given + * analysis collection. + */ +function _isAnalysisPartOfOtherAnalyses (analysis, analysisList) { + return _.any(analysisList, function (otherAnalysisModel) { + if (!analysis.equals(otherAnalysisModel)) { + return otherAnalysisModel.findAnalysisById(analysis.get('id')); + } + return false; + }); +} + +module.exports = { + serialize: serialize +}; diff --git a/src/windshaft/map-serializer/anonymous-map-serializer/anonymous-map-serializer.js b/src/windshaft/map-serializer/anonymous-map-serializer/anonymous-map-serializer.js new file mode 100644 index 0000000..0b70ae5 --- /dev/null +++ b/src/windshaft/map-serializer/anonymous-map-serializer/anonymous-map-serializer.js @@ -0,0 +1,19 @@ +var AnalisysSerializer = require('./analysis-serializer'); +var DataviewSerializer = require('./dataviews-serializer'); +var LayerSerializer = require('./layers-serializer'); + +/** + * Transform a map visualization into a json payload compatible with the windshaft API. + */ +function serialize (layersCollection, dataviewsCollection) { + return { + buffersize: { mvt: 0 }, + layers: LayerSerializer.serialize(layersCollection), + dataviews: DataviewSerializer.serialize(dataviewsCollection), + analyses: AnalisysSerializer.serialize(layersCollection, dataviewsCollection) + }; +} + +module.exports = { + serialize: serialize +}; diff --git a/src/windshaft/map-serializer/anonymous-map-serializer/dataviews-serializer.js b/src/windshaft/map-serializer/anonymous-map-serializer/dataviews-serializer.js new file mode 100644 index 0000000..c6c27c4 --- /dev/null +++ b/src/windshaft/map-serializer/anonymous-map-serializer/dataviews-serializer.js @@ -0,0 +1,10 @@ +function serialize (dataviewsCollection) { + return dataviewsCollection.reduce(function (dataviews, dataviewModel) { + dataviews[dataviewModel.get('id')] = dataviewModel.toJSON(); + return dataviews; + }, {}); +} + +module.exports = { + serialize: serialize +}; diff --git a/src/windshaft/map-serializer/anonymous-map-serializer/layers-serializer.js b/src/windshaft/map-serializer/anonymous-map-serializer/layers-serializer.js new file mode 100644 index 0000000..90bdae8 --- /dev/null +++ b/src/windshaft/map-serializer/anonymous-map-serializer/layers-serializer.js @@ -0,0 +1,125 @@ +var _ = require('underscore'); +var LayerTypes = require('../../../geo/map/layer-types.js'); + +var DEFAULT_CARTOCSS_VERSION = '2.1.0'; + +// This types are the understood by the Maps API. +var HTTP_LAYER_TYPE = 'http'; +var PLAIN_LAYER_TYPE = 'plain'; +var MAPNIK_LAYER_TYPE = 'mapnik'; +var TORQUE_LAYER_TYPE = 'torque'; + +/** + * Generate a json payload from a layer collection of a map + */ +function serialize (layersCollection) { + return layersCollection.chain() + .map(_calculateLayerJSON) + .compact() + .value(); +} + +function _calculateLayerJSON (layerModel) { + if (LayerTypes.isTiledLayer(layerModel)) { + return optionsForHTTPLayer(layerModel); + } else if (LayerTypes.isPlainLayer(layerModel)) { + return optionsForPlainLayer(layerModel); + } else if (LayerTypes.isWMSLayer(layerModel)) { + return optionsForWMSLayer(layerModel); + } else if (LayerTypes.isCartoDBLayer(layerModel)) { + return optionsForMapnikLayer(layerModel); + } else if (LayerTypes.isTorqueLayer(layerModel)) { + return optionsForTorqueLayer(layerModel); + } +} + +function optionsForHTTPLayer (layerModel) { + return { + id: layerModel.get('id'), + type: HTTP_LAYER_TYPE, + options: { + urlTemplate: layerModel.get('urlTemplate'), + subdomains: layerModel.get('subdomains'), + tms: layerModel.get('tms') + } + }; +} + +function optionsForWMSLayer (layerModel) { + return { + id: layerModel.get('id'), + type: HTTP_LAYER_TYPE, + options: { + urlTemplate: layerModel.get('urlTemplate'), + tms: true + } + }; +} + +function optionsForPlainLayer (layerModel) { + return { + id: layerModel.get('id'), + type: PLAIN_LAYER_TYPE, + options: { + color: layerModel.get('color'), + imageUrl: layerModel.get('image') + } + }; +} + +function optionsForMapnikLayer (layerModel) { + var options = sharedOptionsForMapnikAndTorqueLayers(layerModel); + options.interactivity = layerModel.getInteractiveColumnNames(); + + if (layerModel.infowindow && layerModel.infowindow.hasFields()) { + options.attributes = { + id: 'cartodb_id', + columns: layerModel.infowindow.getFieldNames() + }; + } + + if (isFinite(layerModel.get('minzoom'))) { + options.minzoom = layerModel.get('minzoom'); + } + + if (isFinite(layerModel.get('maxzoom'))) { + options.maxzoom = layerModel.get('maxzoom'); + } + + if (!_.isEmpty(layerModel.aggregation)) { + _.extend(options, { + aggregation: layerModel.aggregation + }); + } + + return { + id: layerModel.get('id'), + type: MAPNIK_LAYER_TYPE, + options: options + }; +} + +function optionsForTorqueLayer (layerModel) { + var options = sharedOptionsForMapnikAndTorqueLayers(layerModel); + return { + id: layerModel.get('id'), + type: TORQUE_LAYER_TYPE, + options: options + }; +} + +function sharedOptionsForMapnikAndTorqueLayers (layerModel) { + var options = { + cartocss: layerModel.get('cartocss'), + cartocss_version: layerModel.get('cartocss_version') || DEFAULT_CARTOCSS_VERSION + }; + + options.source = { id: layerModel.getSourceId() }; + + if (layerModel.get('sql_wrap')) { + options.sql_wrap = layerModel.get('sql_wrap'); + } + + return options; +} +module.exports = { serialize: serialize }; diff --git a/src/windshaft/map-serializer/named-map-serializer/named-map-serializer.js b/src/windshaft/map-serializer/named-map-serializer/named-map-serializer.js new file mode 100644 index 0000000..13ebfef --- /dev/null +++ b/src/windshaft/map-serializer/named-map-serializer/named-map-serializer.js @@ -0,0 +1,35 @@ +var _ = require('underscore'); +var LayerTypes = require('../../../geo/map/layer-types'); + +/** + * Transform a map visualization into a json payload compatible with the windshaft API. + */ +function serialize (layersCollection, dataviewsCollection) { + // Named map templates include both http, cartodb and torque layers + // so we need to iterate through all the layers in the collection to + // get the indexes rights. Templates are not aware of Google Maps + // base layers, so we have to ignore them to get indexes right. + var layers = layersCollection.filter(function (layer) { + return !LayerTypes.isGoogleMapsBaseLayer(layer); + }); + + return { + buffersize: { + mvt: 0 + }, + styles: _getStylesFromLayers(layers) + }; +} + +function _getStylesFromLayers (layers) { + return _.reduce(layers, function (styles, layer, index) { + if (layer.get('cartocss')) { + styles[index] = layer.get('cartocss'); + } + return styles; + }, {}); +} + +module.exports = { + serialize: serialize +}; diff --git a/src/windshaft/request-tracker.js b/src/windshaft/request-tracker.js new file mode 100644 index 0000000..f6496a5 --- /dev/null +++ b/src/windshaft/request-tracker.js @@ -0,0 +1,47 @@ +var _ = require('underscore'); + +/** + * Keeps track of subsequent equal requests to the Maps API + * @param {number} maxNumberOfRequests Maximum number of subsequent requests allowed + */ +var RequestTracker = function (maxNumberOfRequests) { + this._maxNumberOfRequests = maxNumberOfRequests || 3; + this.reset(); +}; + +RequestTracker.prototype.track = function (request, response) { + if (!this.lastRequestEquals(request) || !this.lastResponseEquals(response)) { + this.reset(); + } + + if (!this.maxNumberOfRequestsReached()) { + this._lastRequest = request; + this._lastResponse = response; + this._numberOfRequests += 1; + } +}; + +RequestTracker.prototype.reset = function () { + this._lastRequest = undefined; + this._lastResponse = undefined; + this._numberOfRequests = 0; +}; + +RequestTracker.prototype.canRequestBePerformed = function (request) { + return !this.maxNumberOfRequestsReached() || + (this.maxNumberOfRequestsReached() && !this.lastRequestEquals(request)); +}; + +RequestTracker.prototype.maxNumberOfRequestsReached = function () { + return this._numberOfRequests === this._maxNumberOfRequests; +}; + +RequestTracker.prototype.lastRequestEquals = function (request) { + return this._lastRequest && this._lastRequest.equals(request); +}; + +RequestTracker.prototype.lastResponseEquals = function (response) { + return _.isEqual(this._lastResponse, response); +}; + +module.exports = RequestTracker; diff --git a/src/windshaft/request.js b/src/windshaft/request.js new file mode 100644 index 0000000..b9916d4 --- /dev/null +++ b/src/windshaft/request.js @@ -0,0 +1,18 @@ +/** + * Simple value object that holds everything need to instantiate a map using the Maps API + */ +var Request = function (payload, params, options) { + this.payload = payload; + this.params = params; + this.options = options; +}; + +Request.prototype.toHashCode = function () { + return JSON.stringify(this.payload) + JSON.stringify(this.params) + JSON.stringify(this.options); +}; + +Request.prototype.equals = function (request) { + return this.toHashCode() === request.toHashCode(); +}; + +module.exports = Request; diff --git a/src/windshaft/response.js b/src/windshaft/response.js new file mode 100644 index 0000000..a434e2f --- /dev/null +++ b/src/windshaft/response.js @@ -0,0 +1,147 @@ +var _ = require('underscore'); +var WindshaftConfig = require('./config'); + +/** + * Wrapper over a server response to a map instantiation giving some utility methods. + * @constructor + * @param {object} windshaftSettings - Object containing the request options. + * @param {string} serverResponse - The json string representing a windshaft response to a map instantiation. + */ +function Response (windshaftSettings, serverResponse) { + this._windshaftSettings = windshaftSettings; + this._layerGroupId = serverResponse.layergroupid; + this._layers = serverResponse.metadata.layers; + this._dataviews = serverResponse.metadata.dataviews; + this._analyses = serverResponse.metadata.analyses; + this._cdnUrl = serverResponse.cdn_url; +} + +/** + * Return the indexes of the layers for a certain type. + * @example + * // layers = [ carto, carto, tiled, plain, tiled, torque]; + * getLayerIndexesByType('mapnik') // [0, 1] + * getLayerIndexesByType('tiled') // [2, 4] + * getLayerIndexesByType('torque') // [5] + * @param {string} Type - The type of the layers: mapnik, torque, plain, tiled. + */ +Response.prototype.getLayerIndexesByType = function getLayerIndexesByType (layerType) { + return _.reduce(this._getLayers(), function (layerIndexes, layer, index) { + if (layer.type === layerType) { + layerIndexes.push(index); + } + return layerIndexes; + }, []); +}; + +/** + * Build the base url to build windshaft map requests. + */ +Response.prototype.getBaseURL = function getBaseURL () { + return [ + this._getHost(), + WindshaftConfig.MAPS_API_BASE_URL, + this._layerGroupId + ].join('/'); +}; + +/** + * Build the base url for static maps. + */ +Response.prototype.getStaticBaseURL = function getStaticBaseURL () { + return [ + this._getHost(), + WindshaftConfig.MAPS_API_BASE_URL, + 'static/center', + this._layerGroupId + ].join('/'); +}; + +Response.prototype._getHost = function _getHost () { + var urlTemplate = this._windshaftSettings.urlTemplate; + var userName = this._windshaftSettings.userName; + var protocol = this.getProtocol(); + var cdnUrl = this._cdnUrl; + var cdnHost = cdnUrl && cdnUrl[protocol]; + var templates = cdnUrl && cdnUrl.templates; + + if (templates && templates[protocol]) { + var template = templates[protocol]; + return template.url + '/' + userName; + } + + if (cdnHost) { + return [protocol, '://', cdnHost, '/', userName].join(''); + } + + return urlTemplate.replace('{user}', userName); +}; + +Response.prototype.getProtocol = function getProtocol () { + return this._isHttps() ? 'https' : 'http'; +}; + +Response.prototype._isHttps = function isHttps () { + return this._windshaftSettings.urlTemplate.indexOf('https') === 0; +}; + +Response.prototype.getSupportedSubdomains = function getSupportedSubdomains () { + var templates = this._cdnUrl && this._cdnUrl.templates; + var protocol = this.getProtocol(); + if (templates && templates[protocol]) { + return templates[protocol].subdomains; + } + return []; +}; + +Response.prototype.getLayerMetadata = function getLayerMetadata (layerIndex) { + var layerMeta = {}; + var layers = this._getLayers(); + if (layers && layers[layerIndex]) { + layerMeta = layers[layerIndex].meta || {}; + } + return layerMeta; +}; + +Response.prototype.getDataviewMetadata = function getDataviewMetadata (dataviewId) { + var dataviews = this._getDataviews(); + if (dataviews && dataviews[dataviewId]) { + return dataviews[dataviewId]; + } + + // Try to get dataview's metatadta from the 'widgets' dictionary inside the metadata of each of the layers + dataviews = {}; + var layersDataviews = _.compact(_.map(this._getLayers(), function (layer) { return layer.widgets; })); + _.each(layersDataviews, function (layerDataviews) { _.extend(dataviews, layerDataviews); }); + + if (dataviews && dataviews[dataviewId]) { + return dataviews[dataviewId]; + } +}; + +Response.prototype.getAnalysisNodeMetadata = function (analysisId) { + var metadata = {}; + var nodes = _.map(this._getAnalyses(), function (analysis) { + return analysis.nodes; + }); + _.each(nodes, function (node) { _.extend(metadata, node); }); + + return metadata[analysisId]; +}; + +/** + * Return the array with all the layers in the response + */ +Response.prototype._getLayers = function _getLayers () { + return this._layers; +}; + +Response.prototype._getDataviews = function _getDataviews () { + return this._dataviews; +}; + +Response.prototype._getAnalyses = function _getAnalyses () { + return this._analyses; +}; + +module.exports = Response; diff --git a/test/fail-tests-if-have-errors-in-src.js b/test/fail-tests-if-have-errors-in-src.js new file mode 100644 index 0000000..d712ffe --- /dev/null +++ b/test/fail-tests-if-have-errors-in-src.js @@ -0,0 +1,21 @@ +// Catch any eventual errors that happens when test suite is setup, and re-throw once the test runner is ready +var _ = require('underscore'); +var orgOnError = window.onerror; +var onErrorArguments = []; + +window.onerror = function () { + onErrorArguments.push(arguments); + if (_.isFunction(orgOnError)) { + return orgOnError.apply(window, arguments); + } +}; + +describe('errors thrown when loading src files', function () { + it('should never ever happen', function () { + onErrorArguments.forEach(function (args) { + // args = {0: errorMsg, 1: srcFilepath, 2: column, 3: row, 4: error} + throw args[4]; // actual err + }); + expect(onErrorArguments).toEqual([]); + }); +}); diff --git a/test/helpers/SpecHelper.js b/test/helpers/SpecHelper.js new file mode 100644 index 0000000..f3480e3 --- /dev/null +++ b/test/helpers/SpecHelper.js @@ -0,0 +1 @@ +__ENV__ = 'test'; diff --git a/test/helpers/mockFactory.js b/test/helpers/mockFactory.js new file mode 100644 index 0000000..e3fa49e --- /dev/null +++ b/test/helpers/mockFactory.js @@ -0,0 +1,59 @@ +var _ = require('underscore'); +var VisModel = require('../../src/vis/vis'); +var AnalysisModel = require('../../src/analysis/analysis-model'); + +// We use a "fake" reference instead of the one in src/analysis/camshaft-reference +// to ensure that tests won't break if the real thing changes +var fakeCamshaftReference = { + getSourceNamesForAnalysisType: function (analysisType) { + var map = { + 'source': [], + 'trade-area': ['source'], + 'estimated-population': ['source'], + 'point-in-polygon': ['points_source', 'polygons_source'], + 'union': ['source'] + }; + if (!map[analysisType]) { + throw new Error('analysis type ' + analysisType + ' not supported'); + } + return map[analysisType]; + }, + + getParamNamesForAnalysisType: function (analysisType) { + var map = { + 'source': ['query'], + 'trade-area': ['kind', 'time'], + 'estimated-population': ['columnName'], + 'point-in-polygon': [], + 'union': ['join_on'] + }; + if (!map[analysisType]) { + throw new Error('analysis type ' + analysisType + ' not supported'); + } + return map[analysisType]; + } +}; + +var createAnalysisModel = function (attrs) { + if (!_.has(attrs, 'type')) { + attrs.type = 'source'; + } + + var model = new AnalysisModel(attrs, { + camshaftReference: fakeCamshaftReference, + engine: { + reload: function () {} + } + }); + + return model; +}; + +function createVisModel () { + return new VisModel(); +} + +module.exports = { + createAnalysisModel: createAnalysisModel, + createVisModel: createVisModel +}; diff --git a/test/install-source-map-support.js b/test/install-source-map-support.js new file mode 100644 index 0000000..331f16c --- /dev/null +++ b/test/install-source-map-support.js @@ -0,0 +1,6 @@ +/** + * See https://github.com/evanw/node-source-map-support#browser-support + * This is expected to be included in a browserify-module to give proper stack traces, based on browserify's source maps. + */ +/* global sourceMapSupport */ +sourceMapSupport.install(); diff --git a/test/spec/analysis/analysis-model.spec.js b/test/spec/analysis/analysis-model.spec.js new file mode 100644 index 0000000..c8aaf04 --- /dev/null +++ b/test/spec/analysis/analysis-model.spec.js @@ -0,0 +1,516 @@ +var _ = require('underscore'); +var Backbone = require('backbone'); +var AnalysisModel = require('../../../src/analysis/analysis-model.js'); +var AnalysisService = require('../../../src/analysis/analysis-service.js'); + +var fakeCamshaftReference = { + getSourceNamesForAnalysisType: function (analysisType) { + var map = { + 'analysis-type-1': ['source1', 'source2'], + 'trade-area': ['source'], + 'estimated-population': ['source'], + 'sql-function': ['source', 'target'] + }; + return map[analysisType]; + }, + getParamNamesForAnalysisType: function (analysisType) { + var map = { + 'analysis-type-1': ['attribute1', 'attribute2'], + 'trade-area': ['kind', 'time'], + 'estimated-population': ['columnName'] + }; + + return map[analysisType]; + } +}; + +var createFakeEngine = function () { + var engine = new Backbone.Model(); + engine.reload = jasmine.createSpy('reload'); + return engine; +}; + +var createFakeAnalysis = function (attrs, engine) { + return new AnalysisModel(attrs, { + engine: engine, + camshaftReference: fakeCamshaftReference + }); +}; + +describe('src/analysis/analysis-model.js', function () { + var engineMock; + + beforeEach(function () { + engineMock = createFakeEngine(); + this.analysisModel = createFakeAnalysis({ + type: 'analysis-type-1', + attribute1: 'value1', + attribute2: 'value2' + }, engineMock); + }); + + describe('.url', function () { + it('should append the api_key param if present (and not use the authToken)', function () { + this.analysisModel.set({ + url: 'http://example.com', + apiKey: 'THE_API_KEY', + authToken: 'THE_AUTH_TOKEN' + }); + + expect(this.analysisModel.url()).toEqual('http://example.com?api_key=THE_API_KEY'); + }); + + it('should append the auth_token param if present (and not use the authToken)', function () { + this.analysisModel.set({ + url: 'http://example.com', + authToken: 'THE_AUTH_TOKEN' + }); + + expect(this.analysisModel.url()).toEqual('http://example.com?auth_token=THE_AUTH_TOKEN'); + }); + }); + + describe('bindings', function () { + describe('on params change', function () { + it('should reload the map', function () { + this.analysisModel.set({ + attribute1: 'newValue1' + }); + + expect(engineMock.reload).toHaveBeenCalled(); + engineMock.reload.calls.reset(); + + this.analysisModel.set({ + attribute2: 'newValue2' + }); + + expect(engineMock.reload).toHaveBeenCalled(); + engineMock.reload.calls.reset(); + + this.analysisModel.set({ + attribute900: 'something' + }); + + expect(engineMock.reload).not.toHaveBeenCalled(); + }); + + it('should be marked as failed if request to reload the map fails', function () { + this.analysisModel.set({ + attribute1: 'newValue1', + status: AnalysisModel.STATUS.READY + }); + + // Request to the Maps API fails and error callback is invoked... + engineMock.reload.calls.argsFor(0)[0].error('something bad just happened'); + + expect(this.analysisModel.get('status')).toEqual(AnalysisModel.STATUS.FAILED); + }); + }); + + describe('on type change', function () { + it('should unbind old params and bind new params', function () { + spyOn(this.analysisModel, '_initBinds').and.callThrough(); + spyOn(this.analysisModel, 'unbind').and.callThrough(); + this.analysisModel.set('type', 'new!'); + expect(this.analysisModel.unbind).toHaveBeenCalled(); + expect(this.analysisModel._initBinds).toHaveBeenCalled(); + }); + + it('should reload the map', function () { + this.analysisModel.set('type', 'something'); + expect(engineMock.reload).toHaveBeenCalled(); + }); + + it('should keep listening type change again', function () { + this.analysisModel.set('type', 'something'); + expect(engineMock.reload).toHaveBeenCalled(); + engineMock.reload.calls.reset(); + this.analysisModel.set('type', 'something else'); + expect(engineMock.reload).toHaveBeenCalled(); + }); + }); + + describe('on status change', function () { + var createAnalysisModelNoStatusNoReferences = function (engine) { + var analysisModel = createFakeAnalysis({ id: 'a0' }, engine); + return analysisModel; + }; + + var createAnalysisModelNoStatusWithReferences = function (engine) { + var analysisModel = createFakeAnalysis({ id: 'a0' }, engine); + analysisModel.markAsSourceOf(new Backbone.Model()); + return analysisModel; + }; + + var createAnalysisModelWithStatusNoReferences = function (engine) { + var analysisModel = createFakeAnalysis({ id: 'a0', status: 'foo' }, engine); + return analysisModel; + }; + + var createAnalysisModelWithStatusWithReferences = function (engine) { + var analysisModel = createFakeAnalysis({ id: 'a0', status: 'foo' }, engine); + analysisModel.markAsSourceOf(new Backbone.Model()); + return analysisModel; + }; + + var testCases = [ + { + testName: 'analysis with no previous status and no references', + createAnalysisFn: createAnalysisModelNoStatusNoReferences, + expectedVisReloadWhenStatusIn: [] // no relaod is expected + }, + { + testName: 'analysis with no previous status and some references', + createAnalysisFn: createAnalysisModelNoStatusWithReferences, + expectedVisReloadWhenStatusIn: [] // no reload is expected + }, + { + testName: 'analysis with previous status and no references', + createAnalysisFn: createAnalysisModelWithStatusNoReferences, + expectedVisReloadWhenStatusIn: [] // no reload is expected + }, + { + testName: 'analysis with previous status and references', + createAnalysisFn: createAnalysisModelWithStatusWithReferences, + expectedVisReloadWhenStatusIn: [ AnalysisModel.STATUS.READY ] + } + ]; + + _.forEach(testCases, function (testCase) { + var testName = testCase.testName; + var createAnalysisFn = testCase.createAnalysisFn; + var expectedVisReloadWhenStatusIn = testCase.expectedVisReloadWhenStatusIn; + var notExpectedVisReloadWhenStatusIn = []; + + describe(testName, function () { + var analysisModel; + var engineMock; + + beforeEach(function () { + engineMock = createFakeEngine(); + analysisModel = createAnalysisFn(engineMock); + }); + + _.forEach(AnalysisModel.STATUS, function (status) { + if (expectedVisReloadWhenStatusIn.indexOf(status) < 0) { + notExpectedVisReloadWhenStatusIn.push(status); + } + }); + + _.each(expectedVisReloadWhenStatusIn, function (status) { + it("should reload the engine if analysis is now '" + status + "'", function () { + expect(engineMock.reload).not.toHaveBeenCalled(); + analysisModel.set('status', status); + expect(engineMock.reload).toHaveBeenCalled(); + }); + }, this); + + _.each(notExpectedVisReloadWhenStatusIn, function (status) { + it("should NOT reload the engine if analysis is now '" + status + "'", function () { + expect(engineMock.reload).not.toHaveBeenCalled(); + analysisModel.set('status', status); + expect(engineMock.reload).not.toHaveBeenCalled(); + }); + }, this); + }); + }); + }); + }); + + describe('.findAnalysisById', function () { + it('should find a node in the graph', function () { + var fakeCamshaftReference = { + getSourceNamesForAnalysisType: function (analysisType) { + var map = { + 'analysis-type-1': ['source1', 'source2'], + 'analysis-type-2': [], + 'analysis-type-3': ['source3'], + 'analysis-type-4': [], + 'analysis-type-5': ['source4', 'source5'] + }; + return map[analysisType]; + }, + getParamNamesForAnalysisType: function (analysisType) { + var map = { + 'analysis-type-1': ['a'], + 'analysis-type-2': [], + 'analysis-type-3': [], + 'analysis-type-4': ['a4'], + 'analysis-type-5': [] + }; + + return map[analysisType]; + } + }; + + var analysisService = new AnalysisService({ + engine: engineMock, + camshaftReference: fakeCamshaftReference + }); + var analysisModel = analysisService.analyse({ + id: 'a1', + type: 'analysis-type-1', + params: { + a: 1, + source1: { + id: 'a2', + type: 'analysis-type-2', + params: { + a2: 2 + } + }, + source2: { + id: 'a3', + type: 'analysis-type-3', + params: { + source3: { + id: 'a5', + type: 'analysis-type-5', + params: { + source4: { + id: 'a4', + type: 'analysis-type-4', + params: { + a4: 4 + } + } + } + } + } + } + } + }); + + expect(analysisModel.findAnalysisById('a1')).toEqual(analysisModel); + expect(analysisModel.findAnalysisById('a2').get('id')).toEqual('a2'); + expect(analysisModel.findAnalysisById('a3').get('id')).toEqual('a3'); + expect(analysisModel.findAnalysisById('a5').get('id')).toEqual('a5'); + expect(analysisModel.findAnalysisById('b9')).toBeUndefined(); + }); + }); + + describe('.toJSON', function () { + it('should serialize the graph', function () { + var fakeCamshaftReference = { + getSourceNamesForAnalysisType: function (analysisType) { + var map = { + 'analysis-type-1': ['source1', 'source2'], + 'analysis-type-2': [], + 'analysis-type-3': ['source3'], + 'analysis-type-4': [], + 'analysis-type-5': ['source4', 'source5'] + }; + return map[analysisType]; + }, + getParamNamesForAnalysisType: function (analysisType) { + var map = { + 'analysis-type-1': ['a'], + 'analysis-type-2': ['a2'], + 'analysis-type-3': [], + 'analysis-type-4': ['a4'], + 'analysis-type-5': [] + }; + + return map[analysisType]; + }, + isSourceNameOptionalForAnalysisType: function (analysisType, sourceName) { + return (analysisType === 'analysis-type-5' && sourceName === 'source5'); + } + }; + + var analysisService = new AnalysisService({ + engine: engineMock, + camshaftReference: fakeCamshaftReference + }); + var analysisModel = analysisService.analyse({ + id: 'a1', + type: 'analysis-type-1', + params: { + a: 1, + source1: { + id: 'a2', + type: 'analysis-type-2', + params: { + a2: 2 + } + }, + source2: { + id: 'a3', + type: 'analysis-type-3', + params: { + source3: { + id: 'a4', + type: 'analysis-type-4', + params: { + a4: { + id: 'a5', + type: 'analysis-type-5', + params: { + source4: { + id: 'a6', + type: 'analysis-type-2', + params: { + a2: 2 + } + } + } + } + } + } + } + } + } + }); + + expect(analysisModel.toJSON()).toEqual({ + id: 'a1', + type: 'analysis-type-1', + params: { + a: 1, + source1: { + id: 'a2', + type: 'analysis-type-2', + params: { + a2: 2 + } + }, + source2: { + id: 'a3', + type: 'analysis-type-3', + params: { + source3: { + id: 'a4', + type: 'analysis-type-4', + params: { + a4: { + id: 'a5', + type: 'analysis-type-5', + params: { + source4: { + id: 'a6', + type: 'analysis-type-2', + params: { + a2: 2 + } + } + } + } + } + } + } + } + } + }); + }); + }); + + describe('.isDone', function () { + it('should return true if analysis has been calculated', function () { + this.analysisModel.set('status', AnalysisModel.STATUS.READY); + expect(this.analysisModel.isDone()).toEqual(true); + + this.analysisModel.set('status', AnalysisModel.STATUS.FAILED); + expect(this.analysisModel.isDone()).toEqual(true); + }); + + it('should return false if analysis has NOT been calculated', function () { + this.analysisModel.set('status', AnalysisModel.STATUS.PENDING); + expect(this.analysisModel.isDone()).toEqual(false); + + this.analysisModel.set('status', AnalysisModel.STATUS.WAITING); + expect(this.analysisModel.isDone()).toEqual(false); + + this.analysisModel.set('status', AnalysisModel.STATUS.RUNNING); + expect(this.analysisModel.isDone()).toEqual(false); + }); + }); + + describe('.setOk', function () { + it('should unset error attribute', function () { + this.analysisModel.set('error', 'error'); + this.analysisModel.setOk(); + expect(this.analysisModel.get('error')).toBeUndefined(); + }); + }); + + describe('.setError', function () { + it('should set error attribute', function () { + this.analysisModel.setError('wadus'); + + expect(this.analysisModel.get('error')).toEqual('wadus'); + }); + + it('should set analyis as failed', function () { + this.analysisModel.setError('wadus'); + + expect(this.analysisModel.get('status')).toEqual(AnalysisModel.STATUS.FAILED); + }); + }); + + describe('.getNodes', function () { + var analysisService; + beforeEach(function () { + analysisService = new AnalysisService({ + engine: engineMock, + camshaftReference: fakeCamshaftReference + }); + }); + it('Should return a list of nodes from an analysis', function () { + var analysis = analysisService.analyse( + { + id: 'a2', + type: 'estimated-population', + params: { + columnName: 'estimated_people', + source: { + id: 'a1', + type: 'trade-area', + params: { + kind: 'walk', + time: 300, + source: { + id: 'a0', + type: 'source', + params: { + query: 'SELECT * FROM subway_stops' + } + } + } + } + } + } + ); + var actual = analysis.getNodes(); + expect(actual.length).toEqual(3); + }); + }); + + describe('references tracking', function () { + it('should allow keeping track of models that reference this object', function () { + var model1 = new Backbone.Model(); + var model2 = new Backbone.Model(); + + expect(this.analysisModel.isSourceOfAnyModel()).toBe(false); + + this.analysisModel.markAsSourceOf(model1); + + expect(this.analysisModel.isSourceOfAnyModel()).toBe(true); + + this.analysisModel.markAsSourceOf(model1); + + expect(this.analysisModel.isSourceOfAnyModel()).toBe(true); + + this.analysisModel.markAsSourceOf(model2); + + expect(this.analysisModel.isSourceOfAnyModel()).toBe(true); + + this.analysisModel.unmarkAsSourceOf(model1); + + expect(this.analysisModel.isSourceOfAnyModel()).toBe(true); + + this.analysisModel.unmarkAsSourceOf(model2); + + expect(this.analysisModel.isSourceOfAnyModel()).toBe(false); + }); + }); +}); diff --git a/test/spec/analysis/analysis-poller.spec.js b/test/spec/analysis/analysis-poller.spec.js new file mode 100644 index 0000000..4f596b1 --- /dev/null +++ b/test/spec/analysis/analysis-poller.spec.js @@ -0,0 +1,105 @@ +var _ = require('underscore'); +var Backbone = require('backbone'); +var AnalysisModel = require('../../../src/analysis/analysis-model'); +var AnalysisPoller = require('../../../src/analysis/analysis-poller'); + +describe('src/analysis/analysis-poller', function () { + beforeEach(function () { + jasmine.clock().install(); + + var engineMock = new Backbone.Model(); + this.reference = jasmine.createSpyObj('reference', ['getParamNamesForAnalysisType']); + this.analysisModel1 = new AnalysisModel({ + id: 'a1', + url: 'http://carto.com/foo/bar' + }, { engine: engineMock, camshaftReference: this.reference }); + this.analysisPoller = new AnalysisPoller(); + }); + + afterEach(function () { + jasmine.clock().uninstall(); + }); + + describe('.resetAnalysisNodes', function () { + _.each([AnalysisModel.STATUS.PENDING, AnalysisModel.STATUS.WAITING, AnalysisModel.STATUS.RUNNING], function (status) { + it('should start polling if status of an analysis is "' + status + '"', function () { + this.analysisModel1.set({ + 'status': status + }); + + spyOn(this.analysisModel1, 'fetch').and.callFake(function (options) { + options.success(); + }); + + this.analysisPoller.resetAnalysisNodes([ this.analysisModel1 ]); + + expect(this.analysisModel1.fetch).toHaveBeenCalled(); + expect(this.analysisModel1.fetch.calls.count()).toEqual(1); + + // Wait until next fetch is triggered + jasmine.clock().tick(AnalysisPoller.CONFIG.START_DELAY + 1); + + expect(this.analysisModel1.fetch.calls.count()).toEqual(2); + + // Wait until next fetch is triggered + jasmine.clock().tick(AnalysisPoller.CONFIG.START_DELAY * AnalysisPoller.CONFIG.DELAY_MULTIPLIER + 1); + + expect(this.analysisModel1.fetch.calls.count()).toEqual(3); + }); + }); + + _.each([AnalysisModel.STATUS.READY, AnalysisModel.STATUS.FAILED], function (newStatus) { + it('should stop polling if status of an analysis changes to "' + newStatus + '"', function () { + spyOn(this.analysisModel1, 'fetch').and.callFake(function (options) { + this.analysisModel1.set('status', newStatus, { silent: true }); + options.success(); + }.bind(this)); + + this.analysisModel1.set({ + 'status': 'pending' + }); + + this.analysisPoller.resetAnalysisNodes([ this.analysisModel1 ]); + + expect(this.analysisModel1.fetch).toHaveBeenCalled(); + expect(this.analysisModel1.fetch.calls.count()).toEqual(1); + + // Wait until next fetch is triggered + jasmine.clock().tick(AnalysisPoller.CONFIG.START_DELAY + 1); + + expect(this.analysisModel1.fetch.calls.count()).toEqual(1); + }); + }); + }); + + describe('.reset', function () { + it('should reset all pollers', function () { + this.analysisModel1.set({ + 'status': 'pending' + }); + + spyOn(this.analysisModel1, 'fetch').and.callFake(function (options) { + options.success(); + }); + + this.analysisPoller.resetAnalysisNodes([ this.analysisModel1 ]); + + expect(this.analysisModel1.fetch).toHaveBeenCalled(); + expect(this.analysisModel1.fetch.calls.count()).toEqual(1); + + // Wait until next fetch is triggered + jasmine.clock().tick(AnalysisPoller.CONFIG.START_DELAY + 1); + + // Polling is working + expect(this.analysisModel1.fetch.calls.count()).toEqual(2); + + this.analysisPoller.reset(); + + // Wait until next fetch is supposed to be triggered + jasmine.clock().tick(AnalysisPoller.CONFIG.START_DELAY * AnalysisPoller.CONFIG.DELAY_MULTIPLIER + 1); + + // Polling has been stopped + expect(this.analysisModel1.fetch.calls.count()).toEqual(2); + }); + }); +}); diff --git a/test/spec/analysis/analysis-service.spec.js b/test/spec/analysis/analysis-service.spec.js new file mode 100644 index 0000000..fd6b764 --- /dev/null +++ b/test/spec/analysis/analysis-service.spec.js @@ -0,0 +1,357 @@ +var Backbone = require('backbone'); +var _ = require('underscore'); +var AnalysisService = require('../../../src/analysis/analysis-service'); +var CartoDBLayer = require('../../../src/geo/map/cartodb-layer'); +var Dataview = require('../../../src/dataviews/dataview-model-base'); + +describe('src/analysis/analysis-service.js', function () { + var engineMock = new Backbone.Model(); + var fakeCamshaftReference = { + getSourceNamesForAnalysisType: function (analysisType) { + var map = { + 'trade-area': ['source'], + 'estimated-population': ['source'], + 'sql-function': ['source', 'target'] + }; + return map[analysisType]; + }, + getParamNamesForAnalysisType: function (analysisType) { + var map = { + 'trade-area': ['kind', 'time'], + 'estimated-population': ['columnName'] + }; + return map[analysisType]; + } + }; + beforeEach(function () { + this.analysisService = new AnalysisService({ + engine: engineMock, + camshaftReference: fakeCamshaftReference + }); + }); + + describe('.analyse', function () { + it('should generate and return a new analysis', function () { + var subwayStops = this.analysisService.analyse({ + id: 'a0', + type: 'source', + query: 'SELECT * FROM subway_stops' + }); + + expect(subwayStops.attributes).toEqual({ + id: 'a0', + type: 'source', + query: 'SELECT * FROM subway_stops' + }); + }); + + it('should set attrs on the analysis models', function () { + var analysisService = new AnalysisService({ + engine: new Backbone.Model(), + apiKey: 'THE_API_KEY', + authToken: 'THE_AUTH_TOKEN', + camshaftReference: fakeCamshaftReference + }); + + var analysisModel = analysisService.analyse({ + id: 'a0', + type: 'source', + query: 'SELECT * FROM subway_stops' + }); + + expect(analysisModel.get('apiKey')).toEqual('THE_API_KEY'); + expect(analysisModel.get('authToken')).toEqual('THE_AUTH_TOKEN'); + }); + + it('should recursively build the analysis graph', function () { + var estimatedPopulation = this.analysisService.analyse( + { + id: 'a2', + type: 'estimated-population', + params: { + columnName: 'estimated_people', + source: { + id: 'a1', + type: 'trade-area', + params: { + kind: 'walk', + time: 300, + source: { + id: 'a0', + type: 'source', + params: { + query: 'SELECT * FROM subway_stops' + } + } + } + } + } + } + ); + var tradeArea = estimatedPopulation.get('source'); + var subwayStops = tradeArea.get('source'); + expect(tradeArea.get('id')).toEqual('a1'); + expect(subwayStops.get('id')).toEqual('a0'); + }); + + it('analysis should be re-created after it has been removed', function () { + var subwayStops1 = this.analysisService.analyse({ + id: 'a0', + type: 'source', + params: { + query: 'SELECT * FROM subway_stops' + } + }); + + subwayStops1.remove(); + + var subwayStops2 = this.analysisService.analyse({ + id: 'a0', + type: 'source', + params: { + query: 'SELECT * FROM subway_stops ' + } + }); + + expect(subwayStops1.cid).not.toEqual(subwayStops2.cid); + }); + }); + + describe('.findNodeById', function () { + it('should traverse the analysis and return an existing node', function () { + var analysisA = this.analysisService.analyse( + { + id: 'a2', + type: 'estimated-population', + params: { + columnName: 'estimated_people', + source: { + id: 'a1', + type: 'trade-area', + params: { + kind: 'walk', + time: 300, + source: { + id: 'a0', + type: 'source', + params: { + query: 'SELECT * FROM subway_stops' + } + } + } + } + } + } + ); + var analysisANodes = analysisA.getNodesCollection(); + + var analysisB = this.analysisService.analyse( + { + id: 'b0', + type: 'source', + params: { + query: 'SELECT * FROM bus_stops' + } + } + ); + + // This specs make easy to know what went wrong when the test fails + expect(this.analysisService.findNodeById('a2').get('id')).toBe('a2'); + expect(this.analysisService.findNodeById('a1').get('id')).toBe('a1'); + expect(this.analysisService.findNodeById('a0').get('id')).toBe('a0'); + expect(this.analysisService.findNodeById('b0').get('id')).toBe('b0'); + + expect(this.analysisService.findNodeById('a2')).toBe(analysisANodes.get('a2')); + expect(this.analysisService.findNodeById('a1')).toBe(analysisANodes.get('a1')); + expect(this.analysisService.findNodeById('a0')).toBe(analysisANodes.get('a0')); + expect(this.analysisService.findNodeById('b0')).toBe(analysisB); + + expect(this.analysisService.findNodeById('c0')).toBeUndefined(); + }); + + it('should return undefined if node is not found', function () { + pending('Included in previous tests. TODO: create new test for this'); + }); + }); + + describe('._getAnalysisAttributesFromAnalysisDefinition', function () { + it('should analyse all source nodes if everyone has params', function () { + var analysisDefinition = { + type: 'trade-area', + params: { + source: 'a0' + } + }; + spyOn(this.analysisService, 'analyse').and.returnValue('node'); + + var result = this.analysisService._getAnalysisAttributesFromAnalysisDefinition(analysisDefinition, this.analysisService.analyse.bind(this)); + + expect(this.analysisService.analyse.calls.count()).toEqual(1); + expect(this.analysisService.analyse).toHaveBeenCalledWith('a0'); + expect(result).toEqual({ + type: 'trade-area', + source: 'node' + }); + }); + + it('should analyse only source nodes that has params', function () { + var analysisDefinition = { + type: 'sql-function', + params: { + source: 'a0' + } + }; + spyOn(this.analysisService, 'analyse').and.returnValue('node'); + + var result = this.analysisService._getAnalysisAttributesFromAnalysisDefinition(analysisDefinition, this.analysisService.analyse.bind(this)); + + expect(this.analysisService.analyse.calls.count()).toEqual(1); + expect(this.analysisService.analyse).toHaveBeenCalledWith('a0'); + expect(result).toEqual({ + type: 'sql-function', + source: 'node' + }); + }); + }); + + describe('.getUniqueAnalysisNodes', function () { + it('should return the analysis nodes: (single analysis node in a single layer)', function () { + var analysis = this.analysisService.analyse({ + id: 'a0', + type: 'source', + query: 'SELECT * FROM subway_stops' + }); + var layer = new CartoDBLayer({ source: analysis }, { engine: engineMock }); + var layersCollection = new Backbone.Collection([layer]); + var dataviewsCollection = new Backbone.Collection(); + + var expected = analysis; + var actual = AnalysisService.getUniqueAnalysisNodes(layersCollection, dataviewsCollection); + + expect(actual[0]).toEqual(expected); + }); + + it('should return the analysis nodes: (2 analysis nodes, 1 dataview, 1 layer)', function () { + var analysis0 = this.analysisService.analyse({ + id: 'a0', + type: 'source', + query: 'SELECT * FROM subway_stops' + }); + + var analysis1 = this.analysisService.analyse({ + id: 'a1', + type: 'source', + query: 'SELECT * FROM bus_stops' + }); + + var layer = new CartoDBLayer({ source: analysis0 }, { engine: engineMock }); + var dataview = new Dataview({ id: 'dataview1', source: analysis1 }, { map: {}, engine: engineMock }); + + var layersCollection = new Backbone.Collection([layer]); + var dataviewsCollection = new Backbone.Collection([dataview]); + + var expected = [analysis0, analysis1]; + var actual = AnalysisService.getUniqueAnalysisNodes(layersCollection, dataviewsCollection); + + expect(actual.length).toEqual(expected.length); + expect(actual).toEqual(expected); + }); + + it('should return the analysis nodes: (2 analysis nodes, 1 dataview, 1 layer)', function () { + var analysis0 = this.analysisService.analyse({ + id: 'a0', + type: 'source', + query: 'SELECT * FROM subway_stops' + }); + + var analysis1 = this.analysisService.analyse({ + id: 'a1', + type: 'source', + query: 'SELECT * FROM bus_stops' + }); + + var layer = new CartoDBLayer({ source: analysis0 }, { engine: engineMock }); + var dataview = new Dataview({ id: 'dataview1', source: analysis1 }, { map: {}, engine: engineMock }); + + var layersCollection = new Backbone.Collection([layer]); + var dataviewsCollection = new Backbone.Collection([dataview]); + + var expected = [analysis0, analysis1]; + var actual = AnalysisService.getUniqueAnalysisNodes(layersCollection, dataviewsCollection); + + expect(actual.length).toEqual(expected.length); + expect(actual).toEqual(expected); + }); + + it('Should return the analysis nodes: (3 analysis nodes, 1 dataview, 2 layers)', function () { + var analysisA = this.analysisService.analyse( + { + id: 'a2', + type: 'estimated-population', + params: { + columnName: 'estimated_people', + source: { + id: 'a1', + type: 'trade-area', + params: { + kind: 'walk', + time: 300, + source: { + id: 'a0', + type: 'source', + params: { + query: 'SELECT * FROM subway_stops' + } + } + } + } + } + } + ); + var analysisNodes = analysisA.getNodesCollection(); + + var analysis0 = analysisNodes.get('a0'); + var analysis1 = analysisNodes.get('a1'); + var analysis2 = analysisNodes.get('a2'); + + var layer0 = new CartoDBLayer({ source: analysis0 }, { engine: engineMock }); + var layer1 = new CartoDBLayer({ source: analysis2 }, { engine: engineMock }); + var dataview = new Dataview({ id: 'dataview1', source: analysis1 }, { map: {}, engine: engineMock }); + + var layersCollection = new Backbone.Collection([layer0, layer1]); + var dataviewsCollection = new Backbone.Collection([dataview]); + + var expected = [analysis0, analysis2, analysis1]; + var actual = AnalysisService.getUniqueAnalysisNodes(layersCollection, dataviewsCollection); + + // This specs make easy to know what went wrong when the test fails. + expect(actual.length).toEqual(expected.length); + expect(actual[0].id).toEqual(expected[0].id); + expect(actual[1].id).toEqual(expected[1].id); + expect(actual[2].id).toEqual(expected[2].id); + + expect(actual).toEqual(expected); + }); + + it('should compact layers and dataviews if they do not have sources. It happens in named maps.', function () { + var analysis = this.analysisService.analyse({ + id: 'a0', + type: 'source', + params: { + query: 'SELECT * FROM subway_stops' + } + }); + var layer = new CartoDBLayer({ source: analysis }, { engine: engineMock }); + var dataview = new Dataview({ id: 'dataview1', source: analysis }, { map: {}, engine: engineMock }); + layer.set('source', undefined, { silent: true }); + dataview.set('source', undefined, { silent: true }); + var layersCollection = new Backbone.Collection([layer]); + var dataviewsCollection = new Backbone.Collection([dataview]); + + var nodes = AnalysisService.getUniqueAnalysisNodes(layersCollection, dataviewsCollection); + + expect(_.isArray(nodes)).toBe(true); + expect(nodes.length).toBe(0); + }); + }); +}); diff --git a/test/spec/analysis/camshaft-reference.spec.js b/test/spec/analysis/camshaft-reference.spec.js new file mode 100644 index 0000000..a68d5c4 --- /dev/null +++ b/test/spec/analysis/camshaft-reference.spec.js @@ -0,0 +1,19 @@ +var camshaftReference = require('../../../src/analysis/camshaft-reference'); + +describe('src/analysis/camshaft-reference', function () { + describe('.getSourceNamesForAnalysisType', function () { + it('should return the source names for a given analyses type', function () { + expect(camshaftReference.getSourceNamesForAnalysisType('source')).toEqual([]); + expect(camshaftReference.getSourceNamesForAnalysisType('point-in-polygon')).toEqual(['points_source', 'polygons_source']); + expect(camshaftReference.getSourceNamesForAnalysisType('trade-area')).toEqual(['source']); + }); + }); + + describe('.getParamNamesForAnalysisType', function () { + it('should return the params names for a given analyses type', function () { + expect(camshaftReference.getParamNamesForAnalysisType('source')).toEqual(['query']); + expect(camshaftReference.getParamNamesForAnalysisType('point-in-polygon')).toEqual(['points_source', 'polygons_source']); + expect(camshaftReference.getParamNamesForAnalysisType('trade-area')).toEqual([ 'source', 'kind', 'time', 'isolines', 'dissolved' ]); + }); + }); +}); diff --git a/test/spec/api/createVis/create-vis.spec.js b/test/spec/api/createVis/create-vis.spec.js new file mode 100644 index 0000000..51869f1 --- /dev/null +++ b/test/spec/api/createVis/create-vis.spec.js @@ -0,0 +1,247 @@ +var $ = require('jquery'); +var createVis = require('../../../../src/api/create-vis'); +var scenarios = require('./scenarios'); +var Loader = require('../../../../src/core/loader'); + +describe('create-vis:', function () { + beforeEach(function () { + this.container = $('
').css('height', '200px'); + this.containerId = this.container[0].id; + $('body').append(this.container); + }); + + afterEach(function () { + this.container.remove(); + }); + + it('should throw errors when required parameters are missing', function () { + expect(function () { + createVis(); + }).toThrowError('a valid DOM element or selector must be provided'); + + expect(function () { + createVis('something'); + }).toThrowError('a valid DOM element or selector must be provided'); + + expect(function () { + createVis(this.containerId); + }.bind(this)).toThrowError('a vizjson URL or object must be provided'); + + expect(function () { + createVis(this.container[0], 'vizjson'); + }.bind(this)).not.toThrowError(); + + expect(function () { + createVis(this.containerId, 'vizjson'); + }.bind(this)).not.toThrowError(); + }); + + it('should use the given vis.json (instead downloading) when the visjson parameter is provided', function () { + spyOn(Loader, 'get'); + var visJson = scenarios.load('basic'); + createVis(this.containerId, visJson); + expect(Loader.get).not.toHaveBeenCalled(); + }); + + it('should download the vizjson file from a URL when the visjson parameter is provided and is a string', function () { + spyOn(Loader, 'get'); + createVis(this.containerId, 'www.example.com/fake_vis.json'); + expect(Loader.get).toHaveBeenCalledWith('www.example.com/fake_vis.json', jasmine.any(Function)); + }); + + describe('Default (no Options)', function () { + it('should get the map center from the visJson', function () { + var visJson = scenarios.load('basic'); + var visModel = createVis(this.containerId, visJson); + expect(visModel.map.get('center')).toEqual(visJson.center); + }); + + it('should get the title from the visJson', function () { + var visJson = scenarios.load('basic'); + var visModel = createVis(this.containerId, visJson); + expect(visModel.get('title')).toEqual(visJson.title); + }); + + it('should get the description from the visJson', function () { + var visJson = scenarios.load('basic'); + var visModel = createVis(this.containerId, visJson); + expect(visModel.get('description')).toEqual(visJson.description); + }); + + it('should initialize the right protocol (https:false)', function () { + var visJson = scenarios.load('basic'); + var visModel = createVis(this.containerId, visJson); + expect(visModel.get('https')).toEqual(false); + }); + + it('should not have interactive features by default', function () { + var visJson = scenarios.load('basic'); + var visModel = createVis(this.containerId, visJson); + expect(visModel.get('interactiveFeatures')).toEqual(false); + }); + + it('should display the "loader" overlay by default [loaderControl, tiles_loader]', function () { + // loaderControl and tiles_loader appear to do the same. + var visJson = scenarios.load('basic'); + var visModel = createVis(this.containerId, visJson); + expect(visModel.overlaysCollection.findWhere({ type: 'loader' })).toBeDefined(); + }); + + it('should display the "logo" by default', function () { + var visJson = scenarios.load('basic'); + var visModel = createVis(this.containerId, visJson); + expect(visModel.overlaysCollection.findWhere({ type: 'logo' })).toBeDefined(); + }); + + it('should not display empty infowindow fields by default', function () { + var visJson = scenarios.load('basic'); + var visModel = createVis(this.containerId, visJson); + expect(visModel.get('showEmptyInfowindowFields')).toEqual(false); + }); + + it('should have legends', function () { + var visJson = scenarios.load('basic'); + var visModel = createVis(this.containerId, visJson); + expect(visModel.settings.get('showLegends')).toEqual(true); + }); + + it('should show layer selector', function () { + var visJson = scenarios.load('basic'); + var visModel = createVis(this.containerId, visJson); + expect(visModel.settings.get('showLayerSelector')).toEqual(true); + expect(visModel.settings.get('layerSelectorEnabled')).toEqual(true); + }); + + it('should allow scrollwheel by default', function () { + var visJson = scenarios.load('basic'); + var visModel = createVis(this.containerId, visJson); + expect(visModel.map.get('scrollwheel')).toEqual(true); + }); + }); + + describe('Options', function () { + describe('skipMapInstantiation', function () { + it('should instantiate map when skipMapInstantiation option is falsy', function (done) { + var visJson = scenarios.load('basic'); + var visModel = createVis(this.containerId, visJson); + setTimeout(function () { + expect(visModel._instantiateMapWasCalled).toEqual(true); + done(); + }, 25); + }); + + it('should NOT instantiate map when skipMapInstantiation option is truthy', function (done) { + var visJson = scenarios.load('basic'); + var visModel = createVis(this.containerId, visJson, { skipMapInstantiation: true }); + setTimeout(function () { + expect(visModel._instantiateMapWasCalled).toEqual(false); + done(); + }, 25); + }); + }); + }); + + describe('VisModel.map', function () { + it('should have the right title', function () { + var visJson = scenarios.load('basic'); + var visModel = createVis(this.containerId, visJson); + expect(visModel.map.get('title')).toEqual(visJson.title); + }); + + it('should have the right description', function () { + var visJson = scenarios.load('basic'); + var visModel = createVis(this.containerId, visJson); + expect(visModel.map.get('description')).toEqual(visJson.description); + }); + + it('should have the right bounds', function () { + var visJson = scenarios.load('basic'); + var visModel = createVis(this.containerId, visJson); + expect(visModel.map.get('view_bounds_sw')).toEqual(visJson.bounds[0]); + expect(visModel.map.get('view_bounds_ne')).toEqual(visJson.bounds[1]); + }); + + it('should have the right zoom', function () { + var visJson = scenarios.load('basic'); + var visModel = createVis(this.containerId, visJson); + expect(visModel.map.get('zoom')).toEqual(visJson.zoom); + }); + + it('should have the right scrollwheel', function () { + var visJson = scenarios.load('basic'); + var visModel = createVis(this.containerId, visJson); + expect(visModel.map.get('scrollwheel')).toEqual(visJson.options.scrollwheel); + }); + + it('should have the right drag', function () { + var visJson = scenarios.load('basic'); + var visModel = createVis(this.containerId, visJson); + expect(visModel.map.get('drag')).toEqual(true); + }); + + it('should have the right provider', function () { + var visJson = scenarios.load('basic'); + var visModel = createVis(this.containerId, visJson); + expect(visModel.map.get('provider')).toEqual('leaflet'); + }); + + it('should have the right feature interactivity', function () { + var visJson = scenarios.load('basic'); + var visModel = createVis(this.containerId, visJson); + expect(visModel.map.get('isFeatureInteractivityEnabled')).toEqual(false); + }); + + it('should have the right render mode', function () { + var visJson = scenarios.load('basic'); + var visModel = createVis(this.containerId, visJson); + expect(visModel.map.get('renderMode')).toEqual(visJson.vector ? 'vector' : 'raster'); + }); + }); + + describe('VisModel.overlays', function () { + it('should have a share overlay', function () { + var visJson = scenarios.load('basic'); + var visModel = createVis(this.containerId, visJson); + // TODO: Review if this overlay is still supported! + expect(visModel.overlaysCollection.findWhere({ type: 'share' })).toBeDefined(); + }); + + it('should have a search overlay', function () { + var visJson = scenarios.load('basic'); + var visModel = createVis(this.containerId, visJson); + expect(visModel.overlaysCollection.findWhere({ type: 'search' })).toBeDefined(); + }); + + it('should have a zoom overlay', function () { + var visJson = scenarios.load('basic'); + var visModel = createVis(this.containerId, visJson); + expect(visModel.overlaysCollection.findWhere({ type: 'zoom' })).toBeDefined(); + }); + + it('should have a loader overlay', function () { + var visJson = scenarios.load('basic'); + var visModel = createVis(this.containerId, visJson); + expect(visModel.overlaysCollection.findWhere({ type: 'loader' })).toBeDefined(); + }); + + it('should have a logo overlay', function () { + var visJson = scenarios.load('basic'); + var visModel = createVis(this.containerId, visJson); + expect(visModel.overlaysCollection.findWhere({ type: 'logo' })).toBeDefined(); + }); + + it('should have a attribution overlay', function () { + var visJson = scenarios.load('basic'); + var visModel = createVis(this.containerId, visJson); + expect(visModel.overlaysCollection.findWhere({ type: 'attribution' })).toBeDefined(); + }); + }); + + describe('VisModel._dataviewsCollection', function () { + it('should not have dataviews', function () { + var visJson = scenarios.load('basic'); + var visModel = createVis(this.containerId, visJson); + expect(visModel._dataviewsCollection.length).toEqual(0); + }); + }); +}); diff --git a/test/spec/api/createVis/scenarios/basic_vis.json.js b/test/spec/api/createVis/scenarios/basic_vis.json.js new file mode 100644 index 0000000..f5ece62 --- /dev/null +++ b/test/spec/api/createVis/scenarios/basic_vis.json.js @@ -0,0 +1,314 @@ +module.exports = { + 'bounds': [ + [ + 38.994, + -8.74622 + ], + [ + 42.3508, + -1.86658 + ] + ], + 'center': [ + 40.67241595, + -5.306396485 + ], + 'datasource': { + 'user_name': 'iago-carto', + 'maps_api_template': 'https://{user}.carto.com:443', + 'stat_tag': 'd71f6316-b2df-4a33-8109-fa80e8fc793d', + 'template_name': 'tpl_d71f6316_b2df_4a33_8109_fa80e8fc793d' + }, + 'description': null, + 'options': { + 'legends': true, + 'scrollwheel': true, + 'layer_selector': true, + 'dashboard_menu': true + }, + 'id': 'd71f6316-b2df-4a33-8109-fa80e8fc793d', + 'layers': [ + { + 'id': '5bdcb792-78ce-4875-8ab1-869561916426', + 'type': 'tiled', + 'options': { + 'default': 'true', + 'url': 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_nolabels/{z}/{x}/{y}.png', + 'subdomains': 'abcd', + 'minZoom': '0', + 'maxZoom': '18', + 'name': 'Positron', + 'className': 'positron_rainbow_labels', + 'attribution': "© OpenStreetMap contributors", + 'labels': { + 'url': 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_only_labels/{z}/{x}/{y}.png' + }, + 'urlTemplate': 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_nolabels/{z}/{x}/{y}.png' + } + }, + { + 'id': '1b65ba49-e202-4a0a-bb4d-5d8a416788cd', + 'type': 'CartoDB', + 'visible': true, + 'options': { + 'layer_name': 'lugares', + 'attribution': '', + 'cartocss': '#layer {\n marker-width: ramp([population], range(6, 33), quantiles(5));\n marker-fill: #279aff;\n marker-fill-opacity: 0.9;\n marker-allow-overlap: true;\n marker-line-width: 1;\n marker-line-color: #FFF;\n marker-line-opacity: 1;\n}', + 'source': 'a0' + }, + 'infowindow': { + 'template_name': 'infowindow_color', + 'fields': [ + { + 'name': 'population', + 'title': true, + 'position': null + }, + { + 'name': 'description', + 'title': true, + 'position': null + } + ], + 'maxHeight': 180, + 'template': "
\n
\n
\n
\n {{#loading}}\u2026{{/loading}}\n
    \n {{#content.fields}}\n {{^index}}\n
  • \n {{#title}}
    {{title}}
    {{/title}}\n {{#value}}

    {{{ value }}}

    {{/value}}\n
  • \n {{^value}}{{/value}}\n {{/index}}\n {{/content.fields}}\n
\n
\n
\n {{#loading}}\n
\n {{/loading}}\n
    \n {{#content.fields}}\n {{#index}}\n
  • \n {{#title}}\n
    {{title}}
    \n {{/title}}\n {{#value}}\n

    {{{ value }}}

    \n {{/value}}\n {{^value}}\n

    NULL

    \n {{/value}}\n
  • \n {{/index}}\n {{/content.fields}}\n
\n
\n
\n
\n
\n
\n
\n", + 'alternative_names': { + 'name': '', + 'description': '' + }, + 'width': 226, + 'headerColor': { + 'color': { + 'fixed': '#35AAE5', + 'opacity': 1 + } + }, + 'template_type': 'mustache' + }, + 'tooltip': { + 'fields': [ + { + 'name': 'name', + 'title': true, + 'position': null + }, + { + 'name': 'description', + 'title': true, + 'position': null + } + ], + 'template_name': 'tooltip_dark', + 'template': "
\n
    \n {{#fields}}\n
  • \n {{#title}}\n

    {{{ title }}}

    \n {{/title}}\n

    {{{ value }}}

    \n
  • \n {{/fields}}\n
\n
\n", + 'template_type': 'mustache' + }, + 'legends': [ + { + 'conf': { + 'columns': [ + 'title' + ] + }, + 'created_at': '2017-08-31T14:58:44+00:00', + 'definition': { + 'categories': [ + { + 'title': 'Iago', + 'color': '#279aff' + }, + { + 'title': 'Pablo', + 'color': '#e7c5ce' + }, + { + 'title': 'Untitled', + 'color': '#528995' + } + ] + }, + 'id': 'cc81d782-0037-4cba-9575-abe817147bfa', + 'layer_id': '1b65ba49-e202-4a0a-bb4d-5d8a416788cd', + 'post_html': '', + 'pre_html': '', + 'title': 'Wadus', + 'type': 'custom', + 'updated_at': '2017-08-31T14:59:29+00:00' + } + ] + }, + { + 'id': 'a58f47f9-7fb2-4539-a7e6-dcedd7adc9e4', + 'type': 'tiled', + 'options': { + 'default': 'true', + 'url': 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_only_labels/{z}/{x}/{y}.png', + 'subdomains': 'abcd', + 'minZoom': '0', + 'maxZoom': '18', + 'attribution': "© OpenStreetMap contributors", + 'urlTemplate': 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_only_labels/{z}/{x}/{y}.png', + 'type': 'Tiled', + 'name': 'Positron Labels' + } + } + ], + 'likes': 0, + 'map_provider': 'leaflet', + 'overlays': [ + { + 'type': 'share', + 'order': 2, + 'options': { + 'display': true, + 'x': 20, + 'y': 20 + }, + 'template': '' + }, + { + 'type': 'search', + 'order': 3, + 'options': null, + 'template': null + }, + { + 'type': 'zoom', + 'order': 6, + 'options': null, + 'template': null + }, + { + 'type': 'loader', + 'order': 8, + 'options': { + 'display': true, + 'x': 20, + 'y': 150 + }, + 'template': "
" + }, + { + 'type': 'logo', + 'order': 10, + 'options': null, + 'template': null + } + ], + 'title': 'Cities', + 'updated_at': '2017-08-31T15:36:12+00:00', + 'user': { + 'fullname': 'Iago Lastra', + 'avatar_url': 'https://s3.amazonaws.com/com.cartodb.users-assets.production/production/iago-carto/assets/20170720105148Avatar250.png', + 'profile_url': 'https://team.carto.com/u/iago-carto' + }, + 'version': '3.0.0', + 'widgets': [ + { + 'id': '30a96a5d-349d-49c4-875a-c6eba7485635', + 'type': 'category', + 'title': 'name', + 'order': 0, + 'layer_id': '1b65ba49-e202-4a0a-bb4d-5d8a416788cd', + 'options': { + 'column': 'name', + 'aggregation_column': 'name', + 'aggregation': 'count', + 'column_type': 'string', + 'sync_on_bbox_change': true + }, + 'style': { + 'widget_style': { + 'definition': { + 'color': { + 'fixed': '#9DE0AD', + 'opacity': 1 + } + }, + 'widget_color_changed': false + }, + 'auto_style': { + 'custom': false, + 'allowed': true + } + }, + 'source': { + 'id': 'a0' + } + }, + { + 'id': '3d26c92f-0244-4479-9fc0-fb0715283ecd', + 'type': 'histogram', + 'title': 'population', + 'order': 2, + 'layer_id': '1b65ba49-e202-4a0a-bb4d-5d8a416788cd', + 'options': { + 'column': 'population', + 'bins': 10, + 'column_type': 'number', + 'sync_on_bbox_change': true + }, + 'style': { + 'widget_style': { + 'definition': { + 'color': { + 'fixed': '#9DE0AD', + 'opacity': 1 + } + }, + 'widget_color_changed': false + }, + 'auto_style': { + 'custom': false, + 'allowed': true + } + }, + 'source': { + 'id': 'a0' + } + }, + { + 'id': 'd0d44271-43ef-468f-b2e5-64f4a266daa4', + 'type': 'category', + 'title': 'name', + 'order': 3, + 'layer_id': '1b65ba49-e202-4a0a-bb4d-5d8a416788cd', + 'options': { + 'column': 'name', + 'aggregation_column': 'name', + 'aggregation': 'count', + 'column_type': 'string', + 'sync_on_bbox_change': true + }, + 'style': { + 'widget_style': { + 'definition': { + 'color': { + 'fixed': '#9DE0AD', + 'opacity': 1 + } + }, + 'widget_color_changed': false + }, + 'auto_style': { + 'custom': false, + 'allowed': true + } + }, + 'source': { + 'id': 'a0' + } + } + ], + 'zoom': 6, + 'analyses': [ + { + 'id': 'a0', + 'type': 'source', + 'options': { + 'table_name': "'iago-carto'.lugares", + 'simple_geom': 'point' + } + } + ], + 'vector': false +}; diff --git a/test/spec/api/createVis/scenarios/index.js b/test/spec/api/createVis/scenarios/index.js new file mode 100644 index 0000000..d67c4e2 --- /dev/null +++ b/test/spec/api/createVis/scenarios/index.js @@ -0,0 +1,19 @@ +/** + * Load a vis.json from the scenarios folder + * It returns a copy from the object to easy reusing. + */ +function load (index) { + // We use a switch because our current build system doesn't support variables in the require. + switch (index) { + case 'basic': + return clone(require('./basic_vis.json.js')); + } +} + +function clone (object) { + return JSON.parse(JSON.stringify(object)); +} + +module.exports = { + load: load +}; diff --git a/test/spec/api/sql.spec.js b/test/spec/api/sql.spec.js new file mode 100644 index 0000000..8f0f730 --- /dev/null +++ b/test/spec/api/sql.spec.js @@ -0,0 +1,499 @@ +var _ = require('underscore'); +var $ = require('jquery'); +var Backbone = require('backbone'); +var SQL = require('../../../src/api/sql'); + +describe('api/sql', function () { + var USER = 'cartojs-test'; + var sql; + var ajax; + var ajaxParams; + var TEST_DATA = { test: 'good' }; + var NO_BOUNDS = { 'rows': [ + { 'maxx': null } + ]}; + var throwError; + var abort = jasmine.createSpy('abort'); + + beforeEach(function () { + jasmine.clock().install(); + + ajaxParams = null; + ajax = function (params) { + ajaxParams = params; + _.defer(function () { + params.complete && params.complete(); + if (!throwError && params.success) params.success(TEST_DATA, 200); + throwError && params.error && params.error({ + responseText: JSON.stringify({ + error: ['jaja'] + }) + }); + }); + + return { + abort: abort + }; + }; + + spyOn($, 'ajax').and.callFake(ajax); + + sql = new SQL({ + user: USER, + protocol: 'https' + }); + }); + + afterEach(function () { + jasmine.clock().uninstall(); + }); + + it('should compile the url if not completeDomain passed', function () { + expect(sql._host()).toEqual('https://cartojs-test.carto.com/api/v2/sql'); + }); + + it('should compile the url if completeDomain passed', function () { + var sqlBis = new SQL({ + user: USER, + protocol: 'https', + completeDomain: 'http://troloroloro.com' + }); + + expect(sqlBis._host()).toEqual('http://troloroloro.com/api/v2/sql'); + }); + + it('should execute a query', function () { + sql.execute('select * from table'); + expect(ajaxParams.url).toEqual( + 'https://' + USER + '.carto.com/api/v2/sql?q=' + encodeURIComponent('select * from table') + ); + expect(ajaxParams.type).toEqual('get'); + expect(ajaxParams.dataType).toEqual('json'); + expect(ajaxParams.crossDomain).toEqual(true); + }); + + it('should be abortable', function () { + sql = new SQL({ + user: USER, + protocol: 'https', + abortable: true + }); + + sql.execute('select * from table'); + expect(abort).not.toHaveBeenCalled(); + + sql.execute('select * from table limit 10'); + expect(abort).toHaveBeenCalled(); + }); + + it('should parse template', function () { + sql.execute('select * from {{table}}', { + table: 'cartojs-test' + }); + expect(ajaxParams.url).toEqual( + 'https://' + USER + '.carto.com/api/v2/sql?q=' + encodeURIComponent('select * from cartojs-test') + ); + }); + + it('should execute a long query', function () { + // Generating a giant query + var longSQL = []; + var i = 2000; + while (--i) longSQL.push('10000'); + var longQuery = 'SELECT * ' + longSQL; + + // required to have jquery as transport, is checked in the execute method + sql.execute(longQuery); + + expect(ajaxParams.url).toEqual( + 'https://' + USER + '.carto.com/api/v2/sql' + ); + + expect(ajaxParams.data.q).toEqual(longQuery); + expect(ajaxParams.type).toEqual('post'); + expect(ajaxParams.dataType).toEqual('json'); + expect(ajaxParams.crossDomain).toEqual(true); + }); + + it('should execute a long query with params', function () { + var s = new SQL({ + user: 'cartojs-test', + format: 'geojson', + protocol: 'http', + host: 'charlies.com', + api_key: 'testkey', + 'cartojs-test': 'test' + }); + + // Generating a giant query + var longSQL = []; + var i = 2000; + while (--i) longSQL.push('10000'); + var longQuery = 'SELECT * ' + longSQL; + + s.execute(longQuery, null, { + dp: 0 + }); + + expect(ajaxParams.url.indexOf('http://')).not.toEqual(-1); + expect(ajaxParams.url.indexOf('cartojs-test.charlies.com')).not.toEqual(-1); + // Check that we don't have params in the URI + expect(ajaxParams.url.indexOf('&format=geojson')).toEqual(-1); + expect(ajaxParams.url.indexOf('&api_key=testkey')).toEqual(-1); + expect(ajaxParams.url.indexOf('&dp=2')).toEqual(-1); + expect(ajaxParams.url.indexOf('&cartojs-test')).toEqual(-1); + // Check that we have the params in the body + expect(ajaxParams.data.q).toEqual(longQuery); + expect(ajaxParams.data.format).toEqual('geojson'); + expect(ajaxParams.data.api_key).toEqual('testkey'); + expect(ajaxParams.data.dp).toEqual(0); + expect(ajaxParams['cartojs-test']).toEqual('test'); + }); + + it('should substitute mapnik tokens', function () { + sql.execute('select !pixel_width! as w, !pixel_height! as h, !bbox! as b from {{table}}', { + table: 't' + }); + + var earthCircumference = 40075017; + var tileSize = 256; + var srid = 3857; + var fullResolution = earthCircumference / tileSize; + var shift = earthCircumference / 2.0; + + var pw = fullResolution; + var ph = pw; + var bbox = 'ST_MakeEnvelope(' + (-shift) + ',' + (-shift) + ',' + + shift + ',' + shift + ',' + srid + ')'; + + expect(ajaxParams.url).toEqual( + 'https://' + USER + '.carto.com/api/v2/sql?q=' + encodeURIComponent( + 'select ' + pw + ' as w, ' + ph + ' as h, ' + + bbox + ' as b from t') + ); + }); + + it('should call promise', function () { + var data; + var dataCallback; + + sql.execute('select * from bla', function (e, data) { + dataCallback = data; + }).done(function (d) { + data = d; + }); + + jasmine.clock().tick(100); + + expect(data).toEqual(TEST_DATA); + expect(dataCallback).toEqual(TEST_DATA); + }); + + it('should call promise on error', function () { + throwError = true; + var err = false; + + sql.execute('select * from bla').error(function () { + err = true; + }); + + jasmine.clock().tick(10); + expect(err).toEqual(true); + }); + + it('should include url params', function () { + var s = new SQL({ + user: 'cartojs-test', + format: 'geojson', + protocol: 'http', + host: 'charlies.com', + api_key: 'testkey', + 'cartojs-test': 'test' + }); + s.execute('select * from cartojs-test', null, { + dp: 2 + }); + expect(ajaxParams.url.indexOf('http://')).not.toEqual(-1); + expect(ajaxParams.url.indexOf('cartojs-test.charlies.com')).not.toEqual(-1); + expect(ajaxParams.url.indexOf('&format=geojson')).not.toEqual(-1); + expect(ajaxParams.url.indexOf('&api_key=testkey')).not.toEqual(-1); + expect(ajaxParams.url.indexOf('&dp=2')).not.toEqual(-1); + expect(ajaxParams.url.indexOf('&cartojs-test')).toEqual(-1); + }); + + it('should include extra url params', function () { + var s = new SQL({ + user: 'cartojs-test', + format: 'geojson', + protocol: 'http', + host: 'charlies.com', + api_key: 'testkey', + 'cartojs-test': 'test', + extra_params: ['cartojs-test'] + }); + s.execute('select * from cartojs-test', null, { + dp: 2 + }); + expect(ajaxParams.url.indexOf('http://')).not.toEqual(-1); + expect(ajaxParams.url.indexOf('cartojs-test.charlies.com')).not.toEqual(-1); + expect(ajaxParams.url.indexOf('&format=geojson')).not.toEqual(-1); + expect(ajaxParams.url.indexOf('&api_key=testkey')).not.toEqual(-1); + expect(ajaxParams.url.indexOf('&dp=2')).not.toEqual(-1); + expect(ajaxParams.url.indexOf('&cartojs-test=test')).not.toEqual(-1); + + s.execute('select * from cartojs-test', null, { + dp: 2, + 'cartojs-test': 'test2' + }); + expect(ajaxParams.url.indexOf('&cartojs-test=test2')).not.toEqual(-1); + }); + + it('should use jsonp if browser does not support cors', function () { + var corsPrev = $.support.cors; + $.support.cors = false; + var s = new SQL({ user: 'jaja' }); + expect(s.options.jsonp).toEqual(true); + s.execute('select * from cartojs-test', null, { + dp: 2, + jsonpCallback: 'test_callback', + cache: false + }); + expect(ajaxParams.dataType).toEqual('jsonp'); + expect(ajaxParams.crossDomain).toEqual(undefined); + expect(ajaxParams.jsonp).toEqual(undefined); + expect(ajaxParams.jsonpCallback).toEqual('test_callback'); + expect(ajaxParams.cache).toEqual(false); + $.support.cors = corsPrev; + }); + + describe('.getBounds', function () { + it('should get bounds for query', function () { + var sql = 'SELECT ST_XMin(ST_Extent(the_geom)) as minx,' + + ' ST_YMin(ST_Extent(the_geom)) as miny,' + + ' ST_XMax(ST_Extent(the_geom)) as maxx,' + + ' ST_YMax(ST_Extent(the_geom)) as maxy' + + ' from (select * from cartojs-test where id=2) as subq'; + var s = new SQL({ user: 'jaja' }); + s.getBounds('select * from cartojs-test where id={{id}}', {id: 2}); + expect(ajaxParams.url.indexOf(encodeURIComponent(sql))).not.toEqual(-1); + }); + + it('should get bounds for query with appostrophes', function () { + var s = new SQL({ user: 'jaja' }); + s.getBounds('select * from country where name={{ name }}', {name: "'Spain'"}); + expect(ajaxParams.url.indexOf('%26amp%3B%2339%3B')).toEqual(-1); + }); + + it('should resolve promise as error in case there are no bounds', function () { + var prevTestData = TEST_DATA; + var actualErrors = null; + TEST_DATA = NO_BOUNDS; + throwError = false; + + var s = new SQL({ user: 'jaja' }); + s.getBounds('SELECT * FROM somewhere') + .error(function (err) { + actualErrors = err; + }); + + jasmine.clock().tick(10); + expect(actualErrors).not.toBeNull(); + expect(actualErrors.length).toBe(1); + expect(actualErrors[0]).toEqual('No bounds'); + + // Cleaning + TEST_DATA = prevTestData; + }); + + it('should trigger the error callback in case there are no bounds', function () { + var prevTestData = TEST_DATA; + var actualErrors = null; + TEST_DATA = NO_BOUNDS; + throwError = false; + + function cb (err) { + actualErrors = err; + } + + var s = new SQL({ user: 'jaja' }); + s.getBounds('SELECT * FROM somewhere', null, null, cb); + + jasmine.clock().tick(10); + expect(actualErrors).not.toBeNull(); + expect(actualErrors.length).toBe(1); + expect(actualErrors[0]).toEqual('No bounds'); + + // Cleaning + TEST_DATA = prevTestData; + }); + }); +}); + +describe('api/sql.table', function () { + var USER = 'cartojs-test'; + var sql; + + beforeEach(function () { + sql = new SQL({ + user: USER, + protocol: 'https' + }); + }); + + it('sql', function () { + var s = sql.table('test'); + expect(s.sql()).toEqual('select * from test'); + s.columns(['age', 'jeta']); + expect(s.sql()).toEqual('select age,jeta from test'); + s.filter('age < 10'); + expect(s.sql()).toEqual('select age,jeta from test where age < 10'); + s.limit(15); + expect(s.sql()).toEqual('select age,jeta from test where age < 10 limit 15'); + s.order_by('age'); + expect(s.sql()).toEqual('select age,jeta from test where age < 10 limit 15 order by age'); + }); +}); + +describe('api/sql column descriptions', function () { + var USER = 'manolo'; + var sql; + + beforeAll(function () { + this.colDate = new Backbone.Model(JSON.parse('{"name":"object_postedtime","type":"date","geometry_type":"point","bbox":[[-28.92163128242129,-201.09375],[75.84516854027044,196.875]],"analyzed":true,"success":true,"stats":{"type":"date","start_time":"2015-02-19T15:13:16.000Z","end_time":"2015-02-22T04:34:05.000Z","range":220849000,"steps":1024,"null_ratio":0,"column":"object_postedtime"}}')); + this.colFloat = new Backbone.Model(JSON.parse('{"name":"asdfd","type":"number","geometry_type":"point"}')); + this.colString = new Backbone.Model(JSON.parse('{"name":"asdfd","type":"string","geometry_type":"point"}')); + this.colGeom = new Backbone.Model(JSON.parse('{"name":"asdfd","type":"geometry","geometry_type":"point"}')); + this.colBoolean = new Backbone.Model(JSON.parse('{"name":"asdfd","type":"boolean","geometry_type":"point"}')); + this.query = 'SELECT * FROM whatevs'; + + sql = new SQL({ + user: USER, + protocol: 'https' + }); + sql.execute = function (sql, callback) { + callback(null, {}); + }; + }); + + it('should deduct correct describe method', function () { + spyOn(sql, 'describeDate'); + sql.describe(this.query, this.colDate, {type: this.colDate.get('type')}, function () {}); + expect(sql.describeDate).toHaveBeenCalled(); + + spyOn(sql, 'describeFloat'); + sql.describe(this.query, this.colFloat, {type: this.colFloat.get('type')}, function () {}); + expect(sql.describeFloat).toHaveBeenCalled(); + + spyOn(sql, 'describeString'); + sql.describe(this.query, this.colString, {type: this.colString.get('type')}, function () {}); + expect(sql.describeString).toHaveBeenCalled(); + + spyOn(sql, 'describeGeom'); + sql.describe(this.query, this.colGeom, {type: this.colGeom.get('type')}, function () {}); + expect(sql.describeGeom).toHaveBeenCalled(); + + spyOn(sql, 'describeBoolean'); + sql.describe(this.query, this.colBoolean, {type: this.colBoolean.get('type')}, function () {}); + expect(sql.describeBoolean).toHaveBeenCalled(); + }); + + describe('string describer', function () { + var description; + beforeAll(function (done) { + sql.execute = function (sql, callback) { + var data = JSON.parse('{"rows":[{"uniq":462,"cnt":487,"null_count":1,"null_ratio":0.002053388090349076,"skew":0.043121149897330596,"array_agg":""}],"time":0.01,"fields":{"uniq":{"type":"number"},"cnt":{"type":"number"},"null_count":{"type":"number"},"null_ratio":{"type":"number"},"skew":{"type":"number"},"array_agg":{"type":"unknown(2287)"}},"total_rows":1}'); + callback(null, data); + }; + var callback = function (e, stuff) { + description = stuff; + done(); + }; + sql.describeString(sql, this.colString, callback); // THE COLS DON'T MATCH!!! + }); + + it('should return correct properties', function () { + expect(description.hist.constructor).toEqual(Array); // Right now it's an empty array because JSON.parse doesn't like our way of notating histograms + expect(description.type).toEqual('string'); + expect(typeof description.null_count).toEqual('number'); + expect(typeof description.distinct).toEqual('number'); + expect(typeof description.null_ratio).toEqual('number'); + expect(typeof description.skew).toEqual('number'); + expect(typeof description.weight).toEqual('number'); + }); + }); + + describe('geometry describer', function () { + var description; + beforeAll(function (done) { + sql.execute = function (sql, callback) { + var data = {'rows': [{'geometry_type': 'ST_Point'}], 'time': 0.035, 'fields': {'geometry_type': {'type': 'string'}}, 'total_rows': 1}; + callback(null, data); + }; + var callback = function (e, stuff) { + description = stuff; + done(); + }; + sql.describeGeom(sql, this.colGeom, callback); + }); + it('should return correct properties', function () { + expect(description.type).toEqual('geom'); + expect(['ST_Point', 'ST_Line', 'ST_Polygon'].indexOf(description.geometry_type) > -1).toBe(true); + }); + }); + + describe('number describer', function () { + var description; + beforeAll(function (done) { + sql.execute = function (sql, callback) { + var data = JSON.parse('{"rows":[{"hist":"{\\"(1,empty,69368)\\",\\"(25,empty,11063)\\"}","min":0,"max":4,"avg":0.3745819397993311,"cnt":89401,"uniq":5,"null_ratio":0,"stddev":0.000009057366328792043,"stddevmean":2.1617223091836313,"dist_type":"U","quantiles":[0,1,2,2,3,4,4],"equalint":[0,0,0,0,0,0,0],"jenks":[0,1,2,3,4],"headtails":[0,1,2,3,4],"cat_hist":"{\\"(1,empty,69368)\\",\\"(25,empty,11063)\\"}"}],"time":1.442,"fields":{"hist":{"type":"unknown(2287)"},"min":{"type":"number"},"max":{"type":"number"},"avg":{"type":"number"},"cnt":{"type":"number"},"uniq":{"type":"number"},"null_ratio":{"type":"number"},"stddev":{"type":"number"},"stddevmean":{"type":"number"},"dist_type":{"type":"string"},"quantiles":{"type":"number[]"},"equalint":{"type":"number[]"},"jenks":{"type":"number[]"},"headtails":{"type":"number[]"},"cat_hist":{"type":"unknown(2287)"}},"total_rows":1}'); + callback(null, data); + }; + var callback = function (e, stuff) { + description = stuff; + done(); + }; + sql.describeFloat(sql, this.colGeom, callback); + }); + it('should return correct properties', function () { + expect(description.type).toEqual('number'); + expect(['A', 'U', 'F', 'J'].indexOf(description.dist_type) > -1).toBe(true); + var i; + var numTypes = ['avg', 'max', 'min', 'stddevmean', 'weight', 'stddev', 'null_ratio', 'count']; + for (i = 0; i < numTypes.length; i++) { + expect(typeof description[numTypes[i]]).toEqual('number'); + } + var arrayTypes = ['quantiles', 'equalint', 'jenks', 'headtails', 'cat_hist', 'hist']; + for (i = 0; i < arrayTypes.length; i++) { + expect(description[arrayTypes[i]].constructor).toEqual(Array); + } + }); + }); + + describe('boolean describer', function () { + var description; + beforeAll(function (done) { + sql.execute = function (sql, callback) { + var data = {'rows': [ + {'true_ratio': 0.3377926421404682, 'null_ratio': 0, 'uniq': 2, 'cnt': 89401} + ], + 'time': 0.251, + 'fields': {'true_ratio': {'type': 'number'}, 'null_ratio': {'type': 'number'}, 'uniq': {'type': 'number'}, 'cnt': {'type': 'number'}}, + 'total_rows': 1 + }; + callback(null, data); + }; + var callback = function (e, stuff) { + description = stuff; + done(); + }; + sql.describeBoolean(sql, this.colGeom, callback); + }); + it('should return correct properties', function () { + expect(description.type).toEqual('boolean'); + expect(typeof description.true_ratio).toEqual('number'); + expect(typeof description.distinct).toEqual('number'); + expect(typeof description.count).toEqual('number'); + expect(typeof description.null_ratio).toEqual('number'); + }); + }); +}); diff --git a/test/spec/api/v4/client.spec.js b/test/spec/api/v4/client.spec.js new file mode 100644 index 0000000..0d453ca --- /dev/null +++ b/test/spec/api/v4/client.spec.js @@ -0,0 +1,459 @@ +/* global L */ +/* global google */ +var _ = require('underscore'); +var carto = require('../../../../src/api/v4'); +var LeafletLayer = require('../../../../src/api/v4/native/leaflet-layer'); +var GoogleMapsMapType = require('../../../../src/api/v4/native/google-maps-map-type'); +var Engine = require('../../../../src/engine'); +var Events = require('../../../../src/api/v4/events'); + +describe('api/v4/client', function () { + var client; + + beforeEach(function () { + client = new carto.Client({ + apiKey: '84fdbd587e4a942510270a48e843b4c1baa11e18', + serverUrl: 'https://cartojs-test.carto.com', + username: 'cartojs-test' + }); + }); + + describe('constructor', function () { + it('should build a new client', function () { + expect(client).toBeDefined(); + expect(client.getLayers()).toEqual([]); + }); + + it('should autogenerate the carto url when is not given', function () { + client = new carto.Client({ + apiKey: '84fdbd587e4a942510270a48e843b4c1baa11e18', + username: 'cartojs-test' + }); + + expect(client._engine._windshaftSettings.urlTemplate).toEqual('https://cartojs-test.carto.com'); + }); + + it('should autogenerate the carto url when a template is given', function () { + client = new carto.Client({ + apiKey: '84fdbd587e4a942510270a48e843b4c1baa11e18', + username: 'cartojs-test', + serverUrl: 'https://{username}.mycarto.com' + }); + + expect(client._engine._windshaftSettings.urlTemplate).toEqual('https://cartojs-test.mycarto.com'); + }); + + it('should accept a ipv4/user/{username} as a valid serverURL', function () { + expect(function () { + client = new carto.Client({ + apiKey: '84fdbd587e4a942510270a48e843b4c1baa11e18', + username: 'cartojs-test', + serverUrl: 'https://192.168.0.1/user/cartojs-test' + }); + }).not.toThrow(); + }); + + it('should reject a valid ip adress with no /user/{username} as serverURL', function () { + expect(function () { + client = new carto.Client({ + apiKey: '84fdbd587e4a942510270a48e843b4c1baa11e18', + username: 'cartojs-test', + serverUrl: 'https://10.10.0.1' + }); + }).toThrow(); + }); + + it('should reject an invalid ip adress with no /user/{username}', function () { + expect(function () { + client = new carto.Client({ + apiKey: '84fdbd587e4a942510270a48e843b4c1baa11e18', + username: 'cartojs-test', + serverUrl: 'https://192.168.1' + }); + }).toThrow(); + }); + + describe('error handling', function () { + describe('apiKey', function () { + it('should throw a descriptive error when apikey is not given', function () { + expect(function () { + new carto.Client({ username: "cartojs-test" }); // eslint-disable-line + }).toThrowError('apiKey property is required.'); + }); + + it('should throw a descriptive error when apikey is not a string', function () { + expect(function () { + new carto.Client({ apiKey: 1234, username: "cartojs-test" }); // eslint-disable-line + }).toThrowError('apiKey property must be a string.'); + }); + + it('should throw a descriptive error when apikey is not a string', function () { + expect(function () { + new carto.Client({ apiKey: '84fdbd587e4a942510270a48e843b4c1baa11e18' }); // eslint-disable-line + }).toThrowError('username property is required.'); + }); + }); + + describe('username', function () { + it('should throw a descriptive error when username is not a string', function () { + expect(function () { + new carto.Client({ apiKey: '84fdbd587e4a942510270a48e843b4c1baa11e18', username: 1234 }); // eslint-disable-line + }).toThrowError('username property must be a string.'); + }); + }); + + describe('serverUrl', function () { + it('should throw a descriptive error when serverUrl is given and is not valid', function () { + expect(function () { + // eslint-disable-next-line + new carto.Client({ + apiKey: '84fdbd587e4a942510270a48e843b4c1baa11e18', + username: 'cartojs-test', + serverUrl: 'invalid-url' + }); + }).toThrowError('serverUrl is not a valid URL.'); + }); + + it("should throw a descriptive error when serverUrl doesn't match the username", function () { + expect(function () { + // eslint-disable-next-line + new carto.Client({ + apiKey: '84fdbd587e4a942510270a48e843b4c1baa11e18', + username: 'cartojs-test', + serverUrl: 'https://invald-username.carto.com' + }); + }).toThrowError("serverUrl doesn't match the username."); + }); + }); + }); + }); + + describe('.addLayer', function () { + var source; + var style; + var layer; + + beforeEach(function () { + source = new carto.source.Dataset('ne_10m_populated_places_simple', { + id: 'a0' + }); + style = new carto.style.CartoCSS('#layer { marker-fill: red; }'); + layer = new carto.layer.Layer(source, style, {}); + }); + + it('should add a new layer', function () { + client.addLayer(layer); + + expect(client.getLayers()[0]).toEqual(layer); + }); + + it('should add a new layer triggering a reload cycle by default', function (done) { + spyOn(client._engine, 'reload').and.callThrough(); + + client.addLayer(layer).then(function () { + expect(client._engine.reload).toHaveBeenCalled(); + expect(client._engine.reload.calls.count()).toEqual(1); + done(); + }); + }); + + it('should return a rejected promise when some error happened', function (done) { + var errorMock = new Error('Error-Mock'); + spyOn(client._engine, 'reload').and.returnValue( + Promise.reject(errorMock) + ); + + client.addLayer(layer).catch(function (error) { + expect(error.message).toEqual(errorMock.message); + done(); + }); + }); + + it('should return a significative error when layer parameter is not a valid layer', function () { + expect(function () { + client.addLayer([]); + }).toThrowError('The given object is not a layer.'); + }); + + it('should throw a descriptive error when two layers with the same id are added', function () { + expect(function () { + client.addLayer(layer); + client.addLayer( + new carto.layer.Layer(source, style, { id: layer.getId() }) + ); + }).toThrowError('A layer with the same ID already exists in the client.'); + }); + }); + + describe('.addLayers', function () { + it('should add a layers array', function () { }); + it('should add a layer array triggering ONE reload cycle by default', function () { }); + it('should add a layers array without triggering a reload cycle when opts.reload is false', function () { }); + it('should return a rejected promise when some error happened', function () { }); + }); + + describe('.getLayers', function () { + it('should return an empty array when there are no layers', function () { + expect(client.getLayers()).toEqual([]); + }); + xit('should return the layers stored in the client', function (done) { + var source = new carto.source.Dataset('ne_10m_populated_places_simple'); + var style = new carto.style.CartoCSS('#layer { marker-fill: red; }'); + var layer = new carto.layer.Layer(source, style, {}); + client.addLayer(layer).then(function () { + expect(client.getLayers()[0]).toEqual(layer); + done(); + }); + }); + }); + + describe('.removeLayer', function () { + it('should throw a descriptive error when the parameter is invalid', function () { + expect(function () { + client.removeLayer({}); + }).toThrowError('The given object is not a layer.'); + }); + + it('¿should throw a descriptive error when layer is not in the client?', function () { + pending('We should decide if this makes sense.'); + var source = new carto.source.Dataset('ne_10m_populated_places_simple'); + var style = new carto.style.CartoCSS('#layer { marker-fill: red; }'); + var layer = new carto.layer.Layer(source, style, {}); + + expect(function () { + client.removeLayer(layer); + }).toThrowError('The layer is not in the client'); + }); + + it('should remove the layer when is in the client', function () { + var source = new carto.source.Dataset('ne_10m_populated_places_simple'); + var style = new carto.style.CartoCSS('#layer { marker-fill: red; }'); + var layer = new carto.layer.Layer(source, style, {}); + client.addLayer(layer); + + expect(client.getLayers().length).toEqual(1); + + client.removeLayer(layer); + + expect(client.getLayers().length).toEqual(0); + }); + }); + + describe('.removeLayers', function () { + it('must remove all layers', function () { + var source = new carto.source.Dataset('ne_10m_populated_places_simple'); + var style = new carto.style.CartoCSS('#layer { marker-fill: red; }'); + var layerA = new carto.layer.Layer(source, style, {}); + var layerB = new carto.layer.Layer(source, style, {}); + var layerC = new carto.layer.Layer(source, style, {}); + client.addLayers([layerA, layerB, layerC]); + + expect(client.getLayers().length).toEqual(3); + + client.removeLayers(client.getLayers()); + + expect(client.getLayers().length).toEqual(0); + }); + }); + + describe('.removeDataview', function () { + var categoryDataview, populationDataview; + + beforeEach(function () { + var source = new carto.source.Dataset('ne_10m_populated_places_simple'); + categoryDataview = new carto.dataview.Category(source, 'adm0name', { + limit: 10, + operation: carto.operation.SUM, + operationColumn: 'pop_max' + }); + + populationDataview = new carto.dataview.Category(source, 'adm1name', { + limit: 10, + operation: carto.operation.SUM, + operationColumn: 'pop_max' + }); + + client.addDataview(categoryDataview); + client.addDataview(populationDataview); + + spyOn(client._engine, 'removeDataview'); + spyOn(categoryDataview, 'disable'); + spyOn(client, '_reload'); + }); + + it('removes the dataview', function () { + expect(client._dataviews.length).toBe(2); + + client.removeDataview(categoryDataview); + + expect(client._dataviews.length).toBe(1); + }); + + it('disables the dataview', function () { + client.removeDataview(categoryDataview); + + expect(client._engine.removeDataview).toHaveBeenCalled(); + }); + + it('triggers a reload cycle', function () { + client.removeDataview(categoryDataview); + + expect(client._reload).toHaveBeenCalled(); + }); + }); + + describe('.moveLayer', function () { + it('should throw a descriptive error when the parameter is invalid', function () { + var source = new carto.source.Dataset('ne_10m_populated_places_simple'); + var style = new carto.style.CartoCSS('#layer { marker-fill: red; }'); + var layer = new carto.layer.Layer(source, style, {}); + + expect(function () { + client.moveLayer({}, 0); + }).toThrowError('The given object is not a layer.'); + + expect(function () { + client.moveLayer(layer, false); + }).toThrowError('index property must be a number.'); + + expect(function () { + client.moveLayer(layer, 1234); + }).toThrowError('index is out of range.'); + }); + + it('should move the layer when is in the client', function () { + var source = new carto.source.Dataset('ne_10m_populated_places_simple'); + var style = new carto.style.CartoCSS('#layer { marker-fill: red; }'); + var layer0 = new carto.layer.Layer(source, style, {}); + var layer1 = new carto.layer.Layer(source, style, {}); + + client.addLayers([layer0, layer1]); + expect(client.getLayers()[0]).toEqual(layer0); + expect(client.getLayers()[1]).toEqual(layer1); + + client.moveLayer(layer0, 1); + expect(client.getLayers()[1]).toEqual(layer0); + expect(client.getLayers()[0]).toEqual(layer1); + }); + }); + + describe('.getLeafletLayer', function () { + var leafletLayer; + + beforeEach(function () { + leafletLayer = client.getLeafletLayer(); + }); + + it('should return an instance of LeafletLayer', function () { + expect(leafletLayer instanceof LeafletLayer).toBe(true); + }); + + it('should return the same object', function () { + expect(leafletLayer === client.getLeafletLayer()).toBe(true); + }); + + it('should return a L.TileLayer', function () { + expect(leafletLayer instanceof L.TileLayer).toBe(true); + }); + + it('should have the OpenStreetMap / Carto attribution', function () { + expect(leafletLayer.getAttribution()).toBe( + '© OpenStreetMap contributors, © CARTO' + ); + }); + + it('should throw an error if Leaflet is not loaded', function () { + var L = _.clone(window.L); + + window.L = undefined; + expect(function () { + client.getLeafletLayer(); + }).toThrowError('Leaflet is required'); + + // Restore window.L + window.L = L; + }); + + it('should throw an error if Leaflet version is <1.0', function () { + var L = _.clone(window.L); + + window.L = { version: '0.7' }; + expect(function () { + client.getLeafletLayer(); + }).toThrowError('Leaflet +1.0 is required'); + + // Restore window.L + window.L = L; + }); + }); + + describe('.getGoogleMapsMapType', function () { + var element; + var mapType; + + beforeEach(function () { + element = document.createElement('div'); + mapType = client.getGoogleMapsMapType(new google.maps.Map(element)); + }); + + afterEach(function () { + element.remove(); + }); + + it('should return an instance of GoogleMapsMapType', function () { + expect(mapType instanceof GoogleMapsMapType).toBe(true); + }); + + it('should return the same object', function () { + expect(mapType === client.getGoogleMapsMapType()).toBe(true); + }); + + it('should return an object with a MapType interface', function () { + expect(mapType.tileSize).toBeDefined(); + expect(mapType.getTile).toBeDefined(); + }); + + it('should throw an error if Google Maps is not loaded', function () { + var google = _.clone(window.google); + + window.google = undefined; + expect(function () { + client.getGoogleMapsMapType(); + }).toThrowError('Google Maps is required'); + + window.google = { maps: undefined }; + expect(function () { + client.getGoogleMapsMapType(); + }).toThrowError('Google Maps is required'); + + // Restore window.google + window.google = google; + }); + + it('should throw an error if Google Maps version is < 3.31', function () { + var google = _.clone(window.google); + + window.google.maps = { version: '2.4' }; + expect(function () { + client.getGoogleMapsMapType(); + }).toThrowError('Google Maps version should be >= 3.31'); + + // Restore window.google + window.google = google; + }); + }); + + describe('engine bindings', function () { + it('should capture engine LAYER_ERROR and trigger own error', function () { + var capturedError; + client.on(Events.ERROR, function (error) { + capturedError = error; + }); + + client._engine._eventEmmitter.trigger(Engine.Events.LAYER_ERROR); + + expect(capturedError).toBeDefined(); + expect(capturedError.name).toEqual('CartoError'); + }); + }); +}); diff --git a/test/spec/api/v4/dataview/base.spec.js b/test/spec/api/v4/dataview/base.spec.js new file mode 100644 index 0000000..1531244 --- /dev/null +++ b/test/spec/api/v4/dataview/base.spec.js @@ -0,0 +1,243 @@ +var DataviewBase = require('../../../../../src/api/v4/dataview/base'); +var status = require('../../../../../src/api/v4/constants').status; +var carto = require('../../../../../src/api/v4/index'); + +function createSourceMock () { + return new carto.source.Dataset('foo'); +} + +function createEngineMock () { + var engine = { + name: 'Engine mock', + reload: function () {} + }; + spyOn(engine, 'reload'); + + return engine; +} + +describe('api/v4/dataview/base', function () { + var base = new DataviewBase(); + + it('.getStatus should return the internal status', function () { + expect(base.getStatus()).toEqual(base._status); + }); + + describe('.isLoading', function () { + it('should return true if loading and false otherwise', function () { + base._status = status.NOT_LOADED; + expect(base.isLoading()).toBe(false); + base._status = status.LOADING; + expect(base.isLoading()).toBe(true); + }); + + it('should return true if loading and false otherwise', function () { + base._status = status.NOT_LOADED; + expect(base.isLoaded()).toBe(false); + base._status = status.LOADED; + expect(base.isLoaded()).toBe(true); + }); + }); + + describe('.hasError', function () { + it('should return true if loading and false otherwise', function () { + base._status = status.NOT_LOADED; + expect(base.hasError()).toBe(false); + base._status = status.ERROR; + expect(base.hasError()).toBe(true); + }); + }); + + describe('.enable', function () { + it('should enable the dataview', function () { + base.enable(); + expect(base._enabled).toBe(true); + }); + + it('should return the dataview', function () { + expect(base.enable()).toBe(base); + }); + }); + + describe('.disable', function () { + it('should disable the dataview', function () { + base.disable(); + expect(base._enabled).toBe(false); + }); + + it('should return the dataview', function () { + expect(base.disable()).toBe(base); + }); + }); + + describe('.isEnabled', function () { + it('should return true if enabled and false otherwise', function () { + base.disable(); + expect(base.isEnabled()).toBe(false); + base.enable(); + expect(base.isEnabled()).toBe(true); + }); + }); + + describe('.getSource', function () { + it('should return the source object', function () { + var source = new carto.source.Dataset('table_name'); + base._source = source; + expect(base.getSource()).toBe(source); + }); + }); + + describe('.setColumn', function () { + it('should set the column name as string', function () { + var column = 'column-test'; + base.setColumn(column); + expect(base._column).toBe(column); + }); + + it('should throw an error if the argument is not string or undefined', function () { + var requiredColumnError; + var stringColumnError; + var emptyColumnError; + + try { base.setColumn(); } catch (error) { requiredColumnError = error; } + try { base.setColumn(12); } catch (error) { stringColumnError = error; } + try { base.setColumn(''); } catch (error) { emptyColumnError = error; } + + expect(requiredColumnError).toEqual(jasmine.objectContaining({ + message: 'Column property is required.', + type: 'dataview', + errorCode: 'validation:dataview:column-required' + })); + expect(stringColumnError).toEqual(jasmine.objectContaining({ + message: 'Column property must be a string.', + type: 'dataview', + errorCode: 'validation:dataview:column-string' + })); + expect(emptyColumnError).toEqual(jasmine.objectContaining({ + message: 'Column property must be not empty.', + type: 'dataview', + errorCode: 'validation:dataview:empty-column' + })); + }); + + it('should return the dataview', function () { + var column = 'column-test'; + expect(base.setColumn(column)).toBe(base); + }); + }); + + describe('.getColumn', function () { + it('should return the column name', function () { + var column = 'column-test2'; + base._column = column; + expect(base.getColumn()).toBe(column); + }); + }); + + describe('.getData', function () { + it('.getData should not be defined in the base dataview', function () { + expect(function () { base.getData(); }).toThrowError(Error, 'getData must be implemented by the particular dataview.'); + }); + }); + + describe('._changeProperty', function () { + it('should set internal property', function () { + base._example = 'something'; + + base._changeProperty('example', 'whatever'); + + expect(base._example).toEqual('whatever'); + }); + + it('should trigger change is there is no internal model', function () { + var eventValue = ''; + base._example = 'something'; + base.on('exampleChanged', function (newValue) { + eventValue = newValue; + }); + + base._changeProperty('example', 'whatever'); + + expect(eventValue).toEqual('whatever'); + }); + + it('should update internal model and trigger a change when the internalModel exists', function () { + var internalModelSpy = jasmine.createSpyObj('internalModelSpy', ['set']); + base._example = 'something'; + base._internalModel = internalModelSpy; + spyOn(base, '_triggerChange'); + + base._changeProperty('example', 'whatever'); + + expect(internalModelSpy.set).toHaveBeenCalledWith('example', 'whatever'); + expect(base._triggerChange).toHaveBeenCalled(); + }); + }); + + describe('.$setEngine', function () { + var engine; + var dataview; + + beforeEach(function () { + // We use Formula for these tests. Any other dataview could be used instead. + dataview = new carto.dataview.Formula(createSourceMock(), 'population', { + operation: carto.operation.MIN + }); + engine = createEngineMock(); + }); + + it('internalModel events should be properly hooked up', function () { + dataview.$setEngine(engine); + var internalModel = dataview._internalModel; + var eventStatus = null; + var eventError = null; + var dataviewError = null; + dataview.on('statusChanged', function (newStatus, error) { + eventStatus = newStatus; + eventError = error; + }); + dataview.on('error', function (error) { + dataviewError = error; + }); + + // Loading + internalModel.trigger('loading'); + + expect(dataview.getStatus()).toEqual('loading'); + expect(eventStatus).toEqual('loading'); + + // Loaded + internalModel.trigger('loaded'); + + expect(dataview.getStatus()).toEqual('loaded'); + expect(eventStatus).toEqual('loaded'); + + // Error + internalModel.trigger('statusError', internalModel, 'an error'); + + expect(dataview.getStatus()).toEqual('error'); + expect(eventStatus).toEqual('error'); + expect(eventError).toEqual('an error'); + expect(dataviewError.name).toEqual('CartoError'); + }); + }); + + describe('add bbox filter', function () { + it('should check if it is a proper object', function () { + function test () { + base.addFilter('invalid_filter'); + } + + expect(test).toThrowError('Filter property is required.'); + }); + + it('should throw an error if an SQL filter is passed', function () { + function test () { + var categoryFilter = new carto.filter.Category('fake_column', { in: ['category_value'] }); + base.addFilter(categoryFilter); + } + + expect(test).toThrowError('Filter property is required.'); + }); + }); +}); diff --git a/test/spec/api/v4/dataview/category.spec.js b/test/spec/api/v4/dataview/category.spec.js new file mode 100644 index 0000000..ac6ddff --- /dev/null +++ b/test/spec/api/v4/dataview/category.spec.js @@ -0,0 +1,438 @@ +var Backbone = require('backbone'); +var _ = require('underscore'); +var carto = require('../../../../../src/api/v4/index'); +var createEngine = require('../../../fixtures/engine.fixture.js'); + +function createInternalModelMock () { + var internalModelMock = { + set: function () {}, + get: function () {} + }; + spyOn(internalModelMock, 'set'); + spyOn(internalModelMock, 'get').and.callFake(function (key) { + switch (key) { + case 'count': return 42; + case 'max': return 9; + case 'min': return 1; + case 'nulls': return 0; + case 'data': return [ + { + name: 'cat1', + value: 1, + agg: false + }, + { + name: 'others', + value: 9, + agg: true + } + ]; + } + }); + _.extend(internalModelMock, Backbone.Events); + + return internalModelMock; +} + +function createSourceMock () { + return new carto.source.Dataset('foo'); +} + +describe('api/v4/dataview/category', function () { + var source = createSourceMock(); + + describe('initialization', function () { + it('source must be provided', function () { + var error; + try { new carto.dataview.Category(); } catch (err) { error = err; } // eslint-disable-line no-new + + expect(error).toEqual(jasmine.objectContaining({ + message: 'Source property is required.', + type: 'dataview', + errorCode: 'validation:dataview:source-required' + })); + }); + + it('column must be provided', function () { + var error; + try { new carto.dataview.Category(source); } catch (err) { error = err; } // eslint-disable-line no-new + + expect(error).toEqual(jasmine.objectContaining({ + message: 'Column property is required.', + type: 'dataview', + errorCode: 'validation:dataview:column-required' + })); + }); + + it('options set to default if not provided', function () { + var column = 'population'; + + var dataview = new carto.dataview.Category(source, column); + + expect(dataview._limit).toEqual(6); + expect(dataview._operation).toEqual(carto.operation.COUNT); + expect(dataview._operationColumn).toEqual('population'); + }); + + it('options set to the provided value', function () { + var dataview = new carto.dataview.Category(source, 'population', { + limit: 10, + operation: carto.operation.AVG, + operationColumn: 'column-test' + }); + + expect(dataview._limit).toEqual(10); + expect(dataview._operation).toEqual(carto.operation.AVG); + expect(dataview._operationColumn).toEqual('column-test'); + }); + + it('throw error if no correct operation is provided', function () { + var error; + var test = function () { + new carto.dataview.Category(source, 'population', { // eslint-disable-line no-new + operation: 'exponential' + }); + }; + + try { test(); } catch (err) { error = err; } + + expect(error).toEqual(jasmine.objectContaining({ + message: 'Operation for category dataview is not valid. Use carto.operation', + type: 'dataview', + errorCode: 'validation:dataview:category-invalid-operation' + })); + }); + }); + + describe('.setLimit', function () { + var dataview; + + beforeEach(function () { + dataview = new carto.dataview.Category(source, 'population'); + }); + + it('checks if limit is valid', function () { + var requiredError; + var numberError; + var positiveError; + + try { dataview.setLimit(); } catch (err) { requiredError = err; } + try { dataview.setLimit('12'); } catch (err) { numberError = err; } + try { dataview.setLimit(0); } catch (err) { positiveError = err; } + + expect(requiredError).toEqual(jasmine.objectContaining({ + message: 'Limit for category dataview is required.', + type: 'dataview', + errorCode: 'validation:dataview:category-limit-required' + })); + expect(numberError).toEqual(jasmine.objectContaining({ + message: 'Limit for category dataview must be a number.', + type: 'dataview', + errorCode: 'validation:dataview:category-limit-number' + })); + expect(positiveError).toEqual(jasmine.objectContaining({ + message: 'Limit for category dataview must be greater than 0.', + type: 'dataview', + errorCode: 'validation:dataview:category-limit-positive' + })); + }); + + it('if limit is valid, it assigns it to property, returns this and nothing else if there is no internaModel', function () { + var returnedObject = dataview.setLimit(10); + + expect(dataview.getLimit()).toEqual(10); + expect(returnedObject).toBe(dataview); + }); + + it('sets limit in internal model if exists', function () { + var internalModelMock = createInternalModelMock(); + dataview._internalModel = internalModelMock; + + dataview.setLimit(1); + + var operationArgs = internalModelMock.set.calls.mostRecent().args; + expect(operationArgs[0]).toEqual('categories'); + expect(operationArgs[1]).toEqual(1); + }); + + it('should trigger a limitChanged event', function () { + var limitChangedSpy = jasmine.createSpy('operationaChangedSpy'); + dataview.on('limitChanged', limitChangedSpy); + + expect(limitChangedSpy).not.toHaveBeenCalled(); + dataview.$setEngine(createEngine()); + dataview.setLimit(7); + + expect(limitChangedSpy).toHaveBeenCalledWith(7); + }); + }); + + describe('.setOperation', function () { + var dataview; + + beforeEach(function () { + dataview = new carto.dataview.Category(source, 'population'); + }); + + it('checks if operation is valid', function () { + var error; + var test = function () { + dataview.setOperation('swordfish'); + }; + + try { test(); } catch (err) { error = err; } + + expect(error).toEqual(jasmine.objectContaining({ + message: 'Operation for category dataview is not valid. Use carto.operation', + type: 'dataview', + errorCode: 'validation:dataview:category-invalid-operation' + })); + }); + + it('if operation is valid, it assigns it to property, returns this and nothing else if there is no internaModel', function () { + var returnedObject = dataview.setOperation(carto.operation.AVG); + + expect(dataview.getOperation()).toEqual(carto.operation.AVG); + expect(returnedObject).toBe(dataview); + }); + + it('sets operation in internal model if exists', function () { + var internalModelMock = createInternalModelMock(); + dataview._internalModel = internalModelMock; + + dataview.setOperation(carto.operation.AVG); + + var operationArgs = internalModelMock.set.calls.mostRecent().args; + expect(operationArgs[0]).toEqual('aggregation'); + expect(operationArgs[1]).toEqual(carto.operation.AVG); + }); + + it('should trigger a operationChanged event', function () { + var operationChangedSpy = jasmine.createSpy('operationaChangedSpy'); + dataview.on('operationChanged', operationChangedSpy); + + expect(operationChangedSpy).not.toHaveBeenCalled(); + dataview.$setEngine(createEngine()); + dataview.setOperation(carto.operation.AVG); + + expect(operationChangedSpy).toHaveBeenCalledWith(carto.operation.AVG); + }); + }); + + describe('.setOperationColumn', function () { + var dataview; + + beforeEach(function () { + dataview = new carto.dataview.Category(source, 'population'); + }); + + it('checks if operation is valid', function () { + var requiredError; + var numberError; + var emptyError; + + try { dataview.setOperationColumn(); } catch (err) { requiredError = err; } + try { dataview.setOperationColumn(12); } catch (err) { numberError = err; } + try { dataview.setOperationColumn(''); } catch (err) { emptyError = err; } + + expect(requiredError).toEqual(jasmine.objectContaining({ + message: 'Operation column for category dataview is required.', + type: 'dataview', + errorCode: 'validation:dataview:category-operation-required' + })); + expect(numberError).toEqual(jasmine.objectContaining({ + message: 'Operation column for category dataview must be a string.', + type: 'dataview', + errorCode: 'validation:dataview:category-operation-string' + })); + expect(emptyError).toEqual(jasmine.objectContaining({ + message: 'Operation column for category dataview must be not empty.', + type: 'dataview', + errorCode: 'validation:dataview:category-operation-empty' + })); + }); + + it('if operation is valid, it assigns it to property, returns this and nothing else if there is no internaModel', function () { + var returnedObject = dataview.setOperationColumn('columnA'); + + expect(dataview.getOperationColumn()).toEqual('columnA'); + expect(returnedObject).toBe(dataview); + }); + + it('sets operation in internal model if exists', function () { + var internalModelMock = createInternalModelMock(); + dataview._internalModel = internalModelMock; + + dataview.setOperationColumn('columnB'); + + var operationArgs = internalModelMock.set.calls.mostRecent().args; + expect(operationArgs[0]).toEqual('aggregation_column'); + expect(operationArgs[1]).toEqual('columnB'); + }); + + it('should trigger a operationColumnChanged event', function () { + var operationColumnChangedSpy = jasmine.createSpy('operationaChangedSpy'); + dataview.on('operationColumnChanged', operationColumnChangedSpy); + + expect(operationColumnChangedSpy).not.toHaveBeenCalled(); + dataview.$setEngine(createEngine()); + dataview.setOperationColumn('column2'); + + expect(operationColumnChangedSpy).toHaveBeenCalledWith('column2'); + }); + }); + + describe('.getData', function () { + var dataview; + + beforeEach(function () { + dataview = new carto.dataview.Category(source, 'population', { + operation: carto.operation.SUM, + operationColumn: 'column-test' + }); + }); + + it('returns null if there is no internalModel', function () { + var data = dataview.getData(); + + expect(data).toBeNull(); + }); + + it('returns data from internalModel', function () { + var internalModelMock = createInternalModelMock(); + dataview._internalModel = internalModelMock; + + var data = dataview.getData(); + + expect(data).toEqual({ + count: 42, + max: 9, + min: 1, + nulls: 0, + operation: carto.operation.SUM, + categories: [ + { + name: 'cat1', + value: 1, + group: false + }, + { + name: 'others', + value: 9, + group: true + } + ] + }); + }); + }); + + describe('.$setEngine', function () { + var engine; + var dataview; + + beforeEach(function () { + dataview = new carto.dataview.Category(source, 'population', { + operation: carto.operation.MIN, + operationColumn: 'column-test' + }); + engine = createEngine(); + }); + + it('creates the internal model', function () { + dataview.disable(); // To test that it passes the ._enabled property to the internal model + dataview.$setEngine(engine); + + var internalModel = dataview.$getInternalModel(); + expect(internalModel.get('source')).toBe(dataview._source.$getInternalModel()); + expect(internalModel.get('column')).toEqual(dataview._column); + expect(internalModel.get('categories')).toEqual(dataview._limit); + expect(internalModel.get('aggregation')).toEqual(dataview._operation); + expect(internalModel.get('aggregation_column')).toEqual(dataview._operationColumn); + expect(internalModel.isEnabled()).toBe(false); + expect(internalModel._engine).toBe(engine); + }); + + it('calling twice to $setEngine does not create another internalModel', function () { + spyOn(dataview, '_createInternalModel').and.callThrough(); + + dataview.$setEngine(engine); + dataview.$setEngine(engine); + + expect(dataview._createInternalModel.calls.count()).toBe(1); + }); + + describe('spatial filters', function () { + it('creates the internal model with BoundingBox filter if provided', function () { + var filter = new carto.filter.BoundingBox(); + dataview.addFilter(filter); + dataview.$setEngine(engine); + + var internalModel = dataview.$getInternalModel(); + expect(internalModel._bboxFilter).toBeDefined(); + expect(internalModel.syncsOnBoundingBoxChanges()).toBe(true); + }); + + it('allows removing a BoundingBox filter', function () { + var filter = new carto.filter.BoundingBox(); + dataview.addFilter(filter); + dataview.$setEngine(engine); + expect(dataview.hasFilter(filter)).toBe(true); + + dataview.removeFilter(filter); + + expect(dataview.hasFilter(filter)).toBe(false); + var internalModel = dataview.$getInternalModel(); + expect(internalModel._bboxFilter).toBeNull(); + expect(internalModel.syncsOnBoundingBoxChanges()).toBe(false); + }); + + it('creates the internal model with Circle filter if provided', function () { + var filter = new carto.filter.Circle(); + dataview.addFilter(filter); + dataview.$setEngine(engine); + + var internalModel = dataview.$getInternalModel(); + expect(internalModel._circleFilter).toBeDefined(); + expect(internalModel.syncsOnCircleChanges()).toBe(true); + }); + + it('allows removing a Circle filter', function () { + var filter = new carto.filter.Circle(); + dataview.addFilter(filter); + dataview.$setEngine(engine); + expect(dataview.hasFilter(filter)).toBe(true); + + dataview.removeFilter(filter); + + expect(dataview.hasFilter(filter)).toBe(false); + var internalModel = dataview.$getInternalModel(); + expect(internalModel._circleFilter).toBeNull(); + expect(internalModel.syncsOnCircleChanges()).toBe(false); + }); + + it('creates the internal model with Polygon filter if provided', function () { + var filter = new carto.filter.Polygon(); + dataview.addFilter(filter); + dataview.$setEngine(engine); + + var internalModel = dataview.$getInternalModel(); + expect(internalModel._polygonFilter).toBeDefined(); + expect(internalModel.syncsOnPolygonChanges()).toBe(true); + }); + + it('allows removing a Polygon filter', function () { + var filter = new carto.filter.Polygon(); + dataview.addFilter(filter); + dataview.$setEngine(engine); + expect(dataview.hasFilter(filter)).toBe(true); + + dataview.removeFilter(filter); + + expect(dataview.hasFilter(filter)).toBe(false); + var internalModel = dataview.$getInternalModel(); + expect(internalModel._polygonFilter).toBeNull(); + expect(internalModel.syncsOnPolygonChanges()).toBe(false); + }); + }); + }); +}); diff --git a/test/spec/api/v4/dataview/formula.spec.js b/test/spec/api/v4/dataview/formula.spec.js new file mode 100644 index 0000000..2c27dd4 --- /dev/null +++ b/test/spec/api/v4/dataview/formula.spec.js @@ -0,0 +1,265 @@ +var Backbone = require('backbone'); +var _ = require('underscore'); +var carto = require('../../../../../src/api/v4/index'); + +function createInternalModelMock () { + var internalModelMock = { + set: function () {}, + get: function () {} + }; + spyOn(internalModelMock, 'set'); + spyOn(internalModelMock, 'get').and.callFake(function (key) { + if (key === 'data') { + return 1234; + } + if (key === 'nulls') { + return 42; + } + }); + _.extend(internalModelMock, Backbone.Events); + + return internalModelMock; +} + +function createSourceMock () { + return new carto.source.Dataset('foo'); +} + +function createEngineMock () { + var engine = { + name: 'Engine mock', + reload: function () {} + }; + spyOn(engine, 'reload'); + + return engine; +} + +describe('api/v4/dataview/formula', function () { + var source = createSourceMock(); + + describe('initialization', function () { + it('source must be provided', function () { + var test = function () { + new carto.dataview.Formula(); // eslint-disable-line no-new + }; + + expect(test).toThrowError(Error, 'Source property is required.'); + }); + + it('column must be provided', function () { + var test = function () { + new carto.dataview.Formula(source); // eslint-disable-line no-new + }; + + expect(test).toThrowError('Column property is required.'); + }); + + it('options set to default if not provided', function () { + var column = 'population'; + + var dataview = new carto.dataview.Formula(source, column); + + expect(dataview._operation).toEqual(carto.operation.COUNT); + }); + + it('options set to the provided value', function () { + var dataview = new carto.dataview.Formula(source, 'population', { + operation: carto.operation.AVG + }); + + expect(dataview._operation).toEqual(carto.operation.AVG); + }); + + it('throw error if no correct operation is provided', function () { + var test = function () { + new carto.dataview.Formula(source, 'population', { // eslint-disable-line no-new + operation: 'exponential' + }); + }; + + expect(test).toThrowError(Error, 'Operation for formula dataview is not valid. Use carto.operation'); + }); + }); + + describe('.setOperation', function () { + var dataview; + + beforeEach(function () { + dataview = new carto.dataview.Formula(source, 'population'); + }); + + it('checks if operation is valid', function () { + var test = function () { + dataview.setOperation('swordfish'); + }; + + expect(test).toThrowError(Error, 'Operation for formula dataview is not valid. Use carto.operation'); + }); + + it('if operation is valid, it assigns it to property, returns this and nothing else if there is no internaModel', function () { + var returnedObject = dataview.setOperation(carto.operation.AVG); + + expect(dataview.getOperation()).toEqual(carto.operation.AVG); + expect(returnedObject).toBe(dataview); + }); + + it('sets operation in internal model if exists', function () { + var internalModelMock = createInternalModelMock(); + dataview._internalModel = internalModelMock; + + dataview.setOperation(carto.operation.AVG); + + var operationArgs = internalModelMock.set.calls.mostRecent().args; + expect(operationArgs[0]).toEqual('operation'); + expect(operationArgs[1]).toEqual(carto.operation.AVG); + }); + + it('should Trigger a operationChanged event when the operation is changed', function () { + var operationChangedSpy = jasmine.createSpy('operationaChangedSpy'); + dataview.on('operationChanged', operationChangedSpy); + + expect(operationChangedSpy).not.toHaveBeenCalled(); + dataview.$setEngine(createEngineMock()); + dataview.setOperation(carto.operation.MAX); + + expect(operationChangedSpy).toHaveBeenCalledWith(carto.operation.MAX); + }); + }); + + describe('.getData', function () { + var dataview; + + beforeEach(function () { + dataview = new carto.dataview.Formula(source, 'population', { + operation: carto.operation.SUM + }); + }); + + it('returns null if there is no internalModel', function () { + var data = dataview.getData(); + + expect(data).toBeNull(); + }); + + it('returns data from internalModel', function () { + var internalModelMock = createInternalModelMock(); + dataview._internalModel = internalModelMock; + + var data = dataview.getData(); + + expect(data).toEqual({ + nulls: 42, + operation: carto.operation.SUM, + result: 1234 + }); + }); + }); + + describe('.$setEngine', function () { + var engine; + var dataview; + + beforeEach(function () { + dataview = new carto.dataview.Formula(source, 'population', { + operation: carto.operation.MIN + }); + engine = createEngineMock(); + }); + + it('creates the internal model', function () { + dataview.disable(); // To test that it passes the ._enabled property to the internal model + dataview.$setEngine(engine); + + var internalModel = dataview.$getInternalModel(); + expect(internalModel.get('source')).toBe(dataview._source.$getInternalModel()); + expect(internalModel.get('column')).toEqual(dataview._column); + expect(internalModel.get('operation')).toEqual(dataview._operation); + expect(internalModel.isEnabled()).toBe(false); + expect(internalModel._engine.name).toEqual('Engine mock'); + }); + + it('calling twice to $setEngine does not create another internalModel', function () { + spyOn(dataview, '_createInternalModel').and.callThrough(); + + dataview.$setEngine(engine); + dataview.$setEngine(engine); + + expect(dataview._createInternalModel.calls.count()).toBe(1); + }); + + describe('spatial filters', function () { + it('creates the internal model with BoundingBox filter if provided', function () { + var filter = new carto.filter.BoundingBox(); + dataview.addFilter(filter); + dataview.$setEngine(engine); + + var internalModel = dataview.$getInternalModel(); + expect(internalModel._bboxFilter).toBeDefined(); + expect(internalModel.syncsOnBoundingBoxChanges()).toBe(true); + }); + + it('allows removing a BoundingBox filter', function () { + var filter = new carto.filter.BoundingBox(); + dataview.addFilter(filter); + dataview.$setEngine(engine); + expect(dataview.hasFilter(filter)).toBe(true); + + dataview.removeFilter(filter); + + expect(dataview.hasFilter(filter)).toBe(false); + var internalModel = dataview.$getInternalModel(); + expect(internalModel._bboxFilter).toBeNull(); + expect(internalModel.syncsOnBoundingBoxChanges()).toBe(false); + }); + + it('creates the internal model with Circle filter if provided', function () { + var filter = new carto.filter.Circle(); + dataview.addFilter(filter); + dataview.$setEngine(engine); + + var internalModel = dataview.$getInternalModel(); + expect(internalModel._circleFilter).toBeDefined(); + expect(internalModel.syncsOnCircleChanges()).toBe(true); + }); + + it('allows removing a Circle filter', function () { + var filter = new carto.filter.Circle(); + dataview.addFilter(filter); + dataview.$setEngine(engine); + expect(dataview.hasFilter(filter)).toBe(true); + + dataview.removeFilter(filter); + + expect(dataview.hasFilter(filter)).toBe(false); + var internalModel = dataview.$getInternalModel(); + expect(internalModel._circleFilter).toBeNull(); + expect(internalModel.syncsOnCircleChanges()).toBe(false); + }); + + it('creates the internal model with Polygon filter if provided', function () { + var filter = new carto.filter.Polygon(); + dataview.addFilter(filter); + dataview.$setEngine(engine); + + var internalModel = dataview.$getInternalModel(); + expect(internalModel._polygonFilter).toBeDefined(); + expect(internalModel.syncsOnPolygonChanges()).toBe(true); + }); + + it('allows removing a Polygon filter', function () { + var filter = new carto.filter.Polygon(); + dataview.addFilter(filter); + dataview.$setEngine(engine); + expect(dataview.hasFilter(filter)).toBe(true); + + dataview.removeFilter(filter); + + expect(dataview.hasFilter(filter)).toBe(false); + var internalModel = dataview.$getInternalModel(); + expect(internalModel._polygonFilter).toBeNull(); + expect(internalModel.syncsOnPolygonChanges()).toBe(false); + }); + }); + }); +}); diff --git a/test/spec/api/v4/dataview/histogram.spec.js b/test/spec/api/v4/dataview/histogram.spec.js new file mode 100644 index 0000000..37df48c --- /dev/null +++ b/test/spec/api/v4/dataview/histogram.spec.js @@ -0,0 +1,452 @@ +var Backbone = require('backbone'); +var _ = require('underscore'); +var carto = require('../../../../../src/api/v4/index'); +var createEngine = require('../../../fixtures/engine.fixture.js'); + +function createHistogramInternalModelMock (options) { + options = options || {}; + _.extend({ + data: null, + nulls: null + }, options); + var internalModelMock = { + set: function () {}, + get: function () {}, + getUnfilteredDataModel: function () {}, + getUnfilteredData: function () { + return [ + { + freq: 23 + }, { + freq: 46 + }, { + } + ]; + } + }; + spyOn(internalModelMock, 'set'); + spyOn(internalModelMock, 'get').and.callFake(function (key) { + if (key === 'data') { + return options.data; + } + if (key === 'nulls') { + return options.nulls; + } + if (key === 'totalAmount') { + return 7654; + } + }); + spyOn(internalModelMock, 'getUnfilteredDataModel').and.returnValue({ + get: function (key) { + if (key === 'nulls') { + return 12; + } + if (key === 'totalAmount') { + return 707; + } + } + }); + _.extend(internalModelMock, Backbone.Events); + + return internalModelMock; +} + +function createSourceMock () { + return new carto.source.Dataset('ne_10m_populated_places_simple'); +} + +describe('api/v4/dataview/histogram', function () { + var source = createSourceMock(); + + describe('initialization', function () { + it('source must be provided', function () { + var test = function () { + new carto.dataview.Histogram(); // eslint-disable-line no-new + }; + + expect(test).toThrowError(Error, 'Source property is required.'); + }); + + it('column must be provided', function () { + var test = function () { + new carto.dataview.Histogram(source); // eslint-disable-line no-new + }; + + expect(test).toThrowError(Error, 'Column property is required.'); + }); + + it('options set to default if not provided', function () { + var column = 'population'; + + var dataview = new carto.dataview.Histogram(source, column); + + expect(dataview._bins).toEqual(10); + }); + + it('options set to the provided value', function () { + var dataview = new carto.dataview.Histogram(source, 'population', { + bins: 808 + }); + + expect(dataview._bins).toEqual(808); + }); + + it('throw error if bins is not a positive integer value', function () { + var test = function () { + new carto.dataview.Histogram(source, 'population', { // eslint-disable-line no-new + bins: 0 + }); + }; + + expect(test).toThrowError(Error, 'Bins must be a positive integer value.'); + }); + + it('throw error if start is present but not end', function () { + var test = function () { + new carto.dataview.Histogram(source, 'population', { // eslint-disable-line no-new + start: 10 + }); + }; + + expect(test).toThrowError(Error, 'Both start and end values must be a number or null.'); + }); + + it('throw error if end is present but not start', function () { + var test = function () { + new carto.dataview.Histogram(source, 'population', { // eslint-disable-line no-new + end: 10 + }); + }; + + expect(test).toThrowError(Error, 'Both start and end values must be a number or null.'); + }); + }); + + describe('.getData', function () { + var dataview; + + beforeEach(function () { + dataview = new carto.dataview.Histogram(source, 'population'); + }); + + it('returns null if there is no internalModel', function () { + var data = dataview.getData(); + + expect(data).toBeNull(); + }); + + it('returns parsed data from the internal model', function () { + dataview._internalModel = createHistogramInternalModelMock({ + data: [ + { + freq: 35 + }, { + freq: 50 + }, { + } + ], + nulls: 42 + }); + + var data = dataview.getData(); + + expect(data.bins.length).toBe(3); + expect(data.bins[0].freq).toBe(35); + expect(data.bins[1].freq).toBe(50); + expect(data.bins[2].freq).toBeUndefined(); + expect(data.bins[0].normalized).toBe(0.7); + expect(data.bins[1].normalized).toBe(1); + expect(data.bins[2].normalized).toBe(0); + expect(data.nulls).toBe(42); + expect(data.totalAmount).toBe(7654); + }); + + it('returns nulls as 0 in case the internal model has no nulls', function () { + dataview._internalModel = createHistogramInternalModelMock({ + data: [ + { + freq: 35 + }, { + freq: 50 + }, { + } + ], + nulls: undefined + }); + + var data = dataview.getData(); + + expect(data.bins.length).toBe(3); + expect(data.bins[0].freq).toBe(35); + expect(data.bins[1].freq).toBe(50); + expect(data.bins[2].freq).toBeUndefined(); + expect(data.bins[0].normalized).toBe(0.7); + expect(data.bins[1].normalized).toBe(1); + expect(data.bins[2].normalized).toBe(0); + expect(data.nulls).toBe(0); + expect(data.totalAmount).toBe(7654); + }); + + it('returns null if internal model has no data', function () { + dataview._internalModel = createHistogramInternalModelMock(); + + var data = dataview.getData(); + + expect(data).toBe(null); + }); + }); + + describe('.setBins', function () { + var dataview; + + beforeEach(function () { + dataview = new carto.dataview.Histogram(source, 'population'); + }); + + it('should validate bins', function () { + var test = function () { + dataview.setBins(-1); + }; + + expect(test).toThrowError(Error, 'Bins must be a positive integer value.'); + }); + + it('should throw error if called with a float number', function () { + var test = function () { + dataview.setBins(15.7); + }; + + expect(test).toThrowError(Error, 'Bins must be a positive integer value.'); + }); + + it('should set bins to internal model as well', function () { + dataview._internalModel = createHistogramInternalModelMock(); + + dataview.setBins(16); + + expect(dataview._internalModel.set).toHaveBeenCalledWith('bins', 16); + expect(dataview.getBins()).toBe(16); // We assert .getBins() as well + + // Clean + dataview._internalModel = null; + }); + + it('should Trigger a binsChanged event when the bins are changed', function () { + var binsChangedSpy = jasmine.createSpy('binsChangedSpy'); + dataview.on('binsChanged', binsChangedSpy); + + expect(binsChangedSpy).not.toHaveBeenCalled(); + dataview.$setEngine(createEngine()); + dataview.setBins(11); + + expect(binsChangedSpy).toHaveBeenCalledWith(11); + }); + }); + + describe('.setStartEnd', function () { + var dataview; + + beforeEach(function () { + dataview = new carto.dataview.Histogram(source, 'population'); + }); + + it('should throw an error if only start is present', function () { + var test = function () { + dataview.setStartEnd(20, null); + }; + + expect(test).toThrowError(Error, 'Both start and end values must be a number or null.'); + }); + + it('should throw an error if only end is present', function () { + var test = function () { + dataview.setStartEnd(null, 30); + }; + + expect(test).toThrowError(Error, 'Both start and end values must be a number or null.'); + }); + + it('should set start and end with a number', function () { + dataview._internalModel = createHistogramInternalModelMock(); + + dataview.setStartEnd(20, 30); + + expect(dataview._internalModel.set).toHaveBeenCalledWith({ start: 20, end: 30 }); + expect(dataview.getStart()).toBe(20); // We assert .getStart() as well + expect(dataview.getEnd()).toBe(30); // We assert .getEnd() as well + + // Clean + dataview._internalModel = null; + }); + + it('should set start and end with null', function () { + dataview._internalModel = createHistogramInternalModelMock(); + + dataview.setStartEnd(null, null); + + expect(dataview._internalModel.set).toHaveBeenCalledWith({ start: null, end: null }); + expect(dataview.getStart()).toBe(undefined); // We assert .getStart() as well + expect(dataview.getEnd()).toBe(undefined); // We assert .getEnd() as well + + // Clean + dataview._internalModel = null; + }); + + it('should trigger a startChanged event when the start is changed', function () { + var startChangedSpy = jasmine.createSpy('startChangedSpy'); + dataview.on('startChanged', startChangedSpy); + + expect(startChangedSpy).not.toHaveBeenCalled(); + dataview.$setEngine(createEngine()); + dataview.setStartEnd(20, 30); + + expect(startChangedSpy).toHaveBeenCalledWith(20); + }); + + it('should trigger a endChanged event when the end is changed', function () { + var endChangedSpy = jasmine.createSpy('endChangedSpy'); + dataview.on('endChanged', endChangedSpy); + + expect(endChangedSpy).not.toHaveBeenCalled(); + dataview.$setEngine(createEngine()); + dataview.setStartEnd(20, 30); + + expect(endChangedSpy).toHaveBeenCalledWith(30); + }); + }); + + describe('.$setEngine', function () { + var engine; + var dataview; + + beforeEach(function () { + dataview = new carto.dataview.Histogram(source, 'population'); + engine = createEngine(); + }); + + it('creates the internal model', function () { + dataview.disable(); // To test that it passes the ._enabled property to the internal model + dataview.setBins(15); + dataview.$setEngine(engine); + + var internalModel = dataview.$getInternalModel(); + expect(internalModel.get('source')).toBe(dataview._source.$getInternalModel()); + expect(internalModel.get('column')).toEqual(dataview._column); + expect(internalModel.get('bins')).toBe(15); + expect(internalModel.isEnabled()).toBe(false); + expect(internalModel._engine).toBe(engine); + }); + + it('calling twice to $setEngine does not create another internalModel', function () { + spyOn(dataview, '_createInternalModel').and.callThrough(); + + dataview.$setEngine(engine); + dataview.$setEngine(engine); + + expect(dataview._createInternalModel.calls.count()).toBe(1); + }); + + describe('spatial filters', function () { + it('creates the internal model with BoundingBox filter if provided', function () { + var filter = new carto.filter.BoundingBox(); + dataview.addFilter(filter); + dataview.$setEngine(engine); + + var internalModel = dataview.$getInternalModel(); + expect(internalModel._bboxFilter).toBeDefined(); + expect(internalModel.syncsOnBoundingBoxChanges()).toBe(true); + }); + + it('allows removing a BoundingBox filter', function () { + var filter = new carto.filter.BoundingBox(); + dataview.addFilter(filter); + dataview.$setEngine(engine); + expect(dataview.hasFilter(filter)).toBe(true); + + dataview.removeFilter(filter); + + expect(dataview.hasFilter(filter)).toBe(false); + var internalModel = dataview.$getInternalModel(); + expect(internalModel._bboxFilter).toBeNull(); + expect(internalModel.syncsOnBoundingBoxChanges()).toBe(false); + }); + + it('creates the internal model with Circle filter if provided', function () { + var filter = new carto.filter.Circle(); + dataview.addFilter(filter); + dataview.$setEngine(engine); + + var internalModel = dataview.$getInternalModel(); + expect(internalModel._circleFilter).toBeDefined(); + expect(internalModel.syncsOnCircleChanges()).toBe(true); + }); + + it('allows removing a Circle filter', function () { + var filter = new carto.filter.Circle(); + dataview.addFilter(filter); + dataview.$setEngine(engine); + expect(dataview.hasFilter(filter)).toBe(true); + + dataview.removeFilter(filter); + + expect(dataview.hasFilter(filter)).toBe(false); + var internalModel = dataview.$getInternalModel(); + expect(internalModel._circleFilter).toBeNull(); + expect(internalModel.syncsOnCircleChanges()).toBe(false); + }); + + it('creates the internal model with Polygon filter if provided', function () { + var filter = new carto.filter.Polygon(); + dataview.addFilter(filter); + dataview.$setEngine(engine); + + var internalModel = dataview.$getInternalModel(); + expect(internalModel._polygonFilter).toBeDefined(); + expect(internalModel.syncsOnPolygonChanges()).toBe(true); + }); + + it('allows removing a Polygon filter', function () { + var filter = new carto.filter.Polygon(); + dataview.addFilter(filter); + dataview.$setEngine(engine); + expect(dataview.hasFilter(filter)).toBe(true); + + dataview.removeFilter(filter); + + expect(dataview.hasFilter(filter)).toBe(false); + var internalModel = dataview.$getInternalModel(); + expect(internalModel._polygonFilter).toBeNull(); + expect(internalModel.syncsOnPolygonChanges()).toBe(false); + }); + }); + }); + + describe('.getDistributionType', function () { + var dataview; + + beforeEach(function () { + dataview = new carto.dataview.Histogram(source, 'population'); + }); + + it('should return null if there is no internal model', function () { + var distribution = dataview.getDistributionType(); + + expect(distribution).toBe(null); + }); + + it('should call to the proper method in internal model', function () { + var internalModel = { + getData: function () {}, + getDistributionType: function () {} + }; + spyOn(internalModel, 'getData').and.returnValue('token'); + spyOn(internalModel, 'getDistributionType').and.returnValue('a'); + dataview._internalModel = internalModel; + + var distribution = dataview.getDistributionType(); + + expect(internalModel.getDistributionType).toHaveBeenCalledWith('token'); + expect(distribution).toEqual('a'); + }); + }); +}); diff --git a/test/spec/api/v4/dataview/time-series.spec.js b/test/spec/api/v4/dataview/time-series.spec.js new file mode 100644 index 0000000..1d1c10f --- /dev/null +++ b/test/spec/api/v4/dataview/time-series.spec.js @@ -0,0 +1,427 @@ +var Backbone = require('backbone'); +var _ = require('underscore'); +var carto = require('../../../../../src/api/v4/index'); +var createEngine = require('../../../fixtures/engine.fixture.js'); + +function createHistogramInternalModelMock (options) { + options = options || {}; + _.extend({ + data: null, + nulls: null + }, options); + var internalModelMock = { + set: function () {}, + get: function () {}, + getUnfilteredDataModel: function () {}, + getUnfilteredData: function () { + return [ + { + freq: 23 + }, { + freq: 46 + }, { + } + ]; + }, + getCurrentOffset: function () { + return 7200; + } + }; + spyOn(internalModelMock, 'set'); + spyOn(internalModelMock, 'get').and.callFake(function (key) { + if (key === 'data') { + return options.data; + } + if (key === 'nulls') { + return options.nulls; + } + if (key === 'totalAmount') { + return 7654; + } + }); + spyOn(internalModelMock, 'getUnfilteredDataModel').and.returnValue({ + get: function (key) { + if (key === 'nulls') { + return 12; + } + if (key === 'totalAmount') { + return 707; + } + } + }); + _.extend(internalModelMock, Backbone.Events); + + return internalModelMock; +} + +function createSourceMock () { + return new carto.source.Dataset('ne_10m_populated_places_simple'); +} + +describe('api/v4/dataview/time-series', function () { + var source = createSourceMock(); + + describe('initialization', function () { + it('source must be provided', function () { + var test = function () { + new carto.dataview.TimeSeries(); // eslint-disable-line no-new + }; + + expect(test).toThrowError(Error, 'Source property is required.'); + }); + + it('column must be provided', function () { + var test = function () { + new carto.dataview.TimeSeries(source); // eslint-disable-line no-new + }; + + expect(test).toThrowError(Error, 'Column property is required.'); + }); + + it('options set to default if not provided', function () { + var column = 'population'; + + var dataview = new carto.dataview.TimeSeries(source, column); + + expect(dataview._aggregation).toEqual(carto.dataview.timeAggregation.AUTO); + expect(dataview._offset).toBe(0); + expect(dataview._localTimezone).toBe(false); + }); + + it('options set to the provided value', function () { + var dataview = new carto.dataview.TimeSeries(source, 'population', { + aggregation: carto.dataview.timeAggregation.QUARTER, + offset: -7, + useLocalTimezone: true + }); + + expect(dataview._aggregation).toEqual(carto.dataview.timeAggregation.QUARTER); + expect(dataview._offset).toBe(-7 * 3600); // Internaly stored in seconds + expect(dataview._localTimezone).toBe(true); + }); + + it('throw error if aggregation is not a proper value', function () { + var test = function () { + new carto.dataview.TimeSeries(source, 'population', { // eslint-disable-line no-new + aggregation: 'terasecond' + }); + }; + + expect(test).toThrowError(Error, 'Time aggregation must be a valid value. Use carto.dataview.timeAggregation.'); + }); + + it('throw error if offset is not a valid hour', function () { + var test = function () { + new carto.dataview.TimeSeries(source, 'population', { // eslint-disable-line no-new + offset: 34 + }); + }; + + expect(test).toThrowError(Error, 'Offset must an integer value between -12 and 14.'); + + test = function () { + new carto.dataview.TimeSeries(source, 'population', { // eslint-disable-line no-new + offset: 10.45 + }); + }; + + expect(test).toThrowError(Error, 'Offset must an integer value between -12 and 14.'); + }); + + it('throw error if localTimezone is not a binary value', function () { + var test = function () { + new carto.dataview.TimeSeries(source, 'population', { // eslint-disable-line no-new + useLocalTimezone: 'Los Angeles' + }); + }; + + expect(test).toThrowError(Error, 'useLocalTimezone must be a boolean value.'); + }); + }); + + describe('.getData', function () { + var dataview; + + beforeEach(function () { + dataview = new carto.dataview.TimeSeries(source, 'population'); + }); + + it('returns null if there is no internalModel', function () { + var data = dataview.getData(); + + expect(data).toBeNull(); + }); + + it('returns parsed data from the internal model', function () { + dataview._internalModel = createHistogramInternalModelMock({ + data: [ + { + freq: 35 + }, { + freq: 50 + }, { + } + ], + nulls: 42 + }); + + var data = dataview.getData(); + + expect(data.bins.length).toBe(3); + expect(data.bins[0].freq).toBe(35); + expect(data.bins[1].freq).toBe(50); + expect(data.bins[2].freq).toBeUndefined(); + expect(data.bins[0].normalized).toBe(0.7); + expect(data.bins[1].normalized).toBe(1); + expect(data.bins[2].normalized).toBe(0); + expect(data.nulls).toBe(42); + expect(data.totalAmount).toBe(7654); + expect(data.offset).toBe(2); + }); + + it('returns nulls as 0 in case the internal model has no nulls', function () { + dataview._internalModel = createHistogramInternalModelMock({ + data: [ + { + freq: 35 + }, { + freq: 50 + }, { + } + ], + nulls: undefined + }); + + var data = dataview.getData(); + + expect(data.bins.length).toBe(3); + expect(data.bins[0].freq).toBe(35); + expect(data.bins[1].freq).toBe(50); + expect(data.bins[2].freq).toBeUndefined(); + expect(data.bins[0].normalized).toBe(0.7); + expect(data.bins[1].normalized).toBe(1); + expect(data.bins[2].normalized).toBe(0); + expect(data.nulls).toBe(0); + expect(data.totalAmount).toBe(7654); + }); + + it('returns null if internal model has no data', function () { + dataview._internalModel = createHistogramInternalModelMock(); + + var data = dataview.getData(); + + expect(data).toBe(null); + }); + }); + + describe('.setAggregation', function () { + var dataview; + + beforeEach(function () { + dataview = new carto.dataview.TimeSeries(source, 'population'); + }); + + it('should validate aggregation', function () { + var test = function () { + dataview.setAggregation('terasecond'); + }; + + expect(test).toThrowError(Error, 'Time aggregation must be a valid value. Use carto.dataview.timeAggregation.'); + }); + + it('should set aggregation to internal model as well', function () { + dataview._internalModel = createHistogramInternalModelMock(); + + dataview.setAggregation(carto.dataview.timeAggregation.HOUR); + + expect(dataview._internalModel.set).toHaveBeenCalledWith('aggregation', carto.dataview.timeAggregation.HOUR); + expect(dataview.getAggregation()).toBe('hour'); + + // Clean + dataview._internalModel = null; + }); + + it('should Trigger a aggregationChanged event when the aggregation are changed', function () { + var aggregationChangedSpy = jasmine.createSpy('aggregationChangedSpy'); + dataview.on('aggregationChanged', aggregationChangedSpy); + + expect(aggregationChangedSpy).not.toHaveBeenCalled(); + dataview.$setEngine(createEngine()); + dataview.setAggregation(carto.dataview.timeAggregation.HOUR); + + expect(aggregationChangedSpy).toHaveBeenCalledWith(carto.dataview.timeAggregation.HOUR); + }); + }); + + describe('.setOffset', function () { + var dataview; + + beforeEach(function () { + dataview = new carto.dataview.TimeSeries(source, 'population'); + }); + + it('should validate offset', function () { + var test = function () { + dataview.setOffset(32); + }; + + expect(test).toThrowError(Error, 'Offset must an integer value between -12 and 14.'); + + test = function () { + dataview.setOffset(10.7); + }; + + expect(test).toThrowError(Error, 'Offset must an integer value between -12 and 14.'); + }); + + it('should set offset to internal model as well translated to seconds', function () { + dataview._internalModel = createHistogramInternalModelMock(); + + dataview.setOffset(-5); + + expect(dataview._internalModel.set).toHaveBeenCalledWith('offset', -5 * 3600); + expect(dataview.getOffset()).toBe(-5); + + // Clean + dataview._internalModel = null; + }); + }); + + describe('.useLocalTimezone', function () { + var dataview; + + beforeEach(function () { + dataview = new carto.dataview.TimeSeries(source, 'population'); + }); + + it('should validate localTimezone', function () { + var test = function () { + dataview.useLocalTimezone('Compton'); + }; + + expect(test).toThrowError(Error, 'useLocalTimezone must be a boolean value.'); + }); + + it('should set localTimezone to internal model as well', function () { + dataview._internalModel = createHistogramInternalModelMock(); + + dataview.useLocalTimezone(true); + + expect(dataview._internalModel.set).toHaveBeenCalledWith('localTimezone', true); + expect(dataview.isUsingLocalTimezone()).toBe(true); + + // Clean + dataview._internalModel = null; + }); + }); + + describe('.$setEngine', function () { + var engine; + var dataview; + + beforeEach(function () { + dataview = new carto.dataview.TimeSeries(source, 'population'); + engine = createEngine(); + }); + + it('creates the internal model', function () { + dataview.disable(); // To test that it passes the ._enabled property to the internal model + dataview.setAggregation(carto.dataview.timeAggregation.WEEK); + dataview.setOffset(6); + dataview.useLocalTimezone(true); + dataview.$setEngine(engine); + + var internalModel = dataview.$getInternalModel(); + expect(internalModel.get('source')).toBe(dataview._source.$getInternalModel()); + expect(internalModel.get('column')).toEqual(dataview._column); + expect(internalModel.get('aggregation')).toBe('week'); + expect(internalModel.get('localTimezone')).toBe(true); + expect(internalModel.get('offset')).toBe(6 * 3600); + expect(internalModel.isEnabled()).toBe(false); + expect(internalModel._engine).toBe(engine); + }); + + it('calling twice to $setEngine does not create another internalModel', function () { + spyOn(dataview, '_createInternalModel').and.callThrough(); + + dataview.$setEngine(engine); + dataview.$setEngine(engine); + + expect(dataview._createInternalModel.calls.count()).toBe(1); + }); + + describe('spatial filters', function () { + it('creates the internal model with BoundingBox filter if provided', function () { + var filter = new carto.filter.BoundingBox(); + dataview.addFilter(filter); + dataview.$setEngine(engine); + + var internalModel = dataview.$getInternalModel(); + expect(internalModel._bboxFilter).toBeDefined(); + expect(internalModel.syncsOnBoundingBoxChanges()).toBe(true); + }); + + it('allows removing a BoundingBox filter', function () { + var filter = new carto.filter.BoundingBox(); + dataview.addFilter(filter); + dataview.$setEngine(engine); + expect(dataview.hasFilter(filter)).toBe(true); + + dataview.removeFilter(filter); + + expect(dataview.hasFilter(filter)).toBe(false); + var internalModel = dataview.$getInternalModel(); + expect(internalModel._bboxFilter).toBeNull(); + expect(internalModel.syncsOnBoundingBoxChanges()).toBe(false); + }); + + it('creates the internal model with Circle filter if provided', function () { + var filter = new carto.filter.Circle(); + dataview.addFilter(filter); + dataview.$setEngine(engine); + + var internalModel = dataview.$getInternalModel(); + expect(internalModel._circleFilter).toBeDefined(); + expect(internalModel.syncsOnCircleChanges()).toBe(true); + }); + + it('allows removing a Circle filter', function () { + var filter = new carto.filter.Circle(); + dataview.addFilter(filter); + dataview.$setEngine(engine); + expect(dataview.hasFilter(filter)).toBe(true); + + dataview.removeFilter(filter); + + expect(dataview.hasFilter(filter)).toBe(false); + var internalModel = dataview.$getInternalModel(); + expect(internalModel._circleFilter).toBeNull(); + expect(internalModel.syncsOnCircleChanges()).toBe(false); + }); + + it('creates the internal model with Polygon filter if provided', function () { + var filter = new carto.filter.Polygon(); + dataview.addFilter(filter); + dataview.$setEngine(engine); + + var internalModel = dataview.$getInternalModel(); + expect(internalModel._polygonFilter).toBeDefined(); + expect(internalModel.syncsOnPolygonChanges()).toBe(true); + }); + + it('allows removing a Polygon filter', function () { + var filter = new carto.filter.Polygon(); + dataview.addFilter(filter); + dataview.$setEngine(engine); + expect(dataview.hasFilter(filter)).toBe(true); + + dataview.removeFilter(filter); + + expect(dataview.hasFilter(filter)).toBe(false); + var internalModel = dataview.$getInternalModel(); + expect(internalModel._polygonFilter).toBeNull(); + expect(internalModel.syncsOnPolygonChanges()).toBe(false); + }); + }); + }); +}); diff --git a/test/spec/api/v4/error-handling/carto-error-extender.spec.js b/test/spec/api/v4/error-handling/carto-error-extender.spec.js new file mode 100644 index 0000000..288a098 --- /dev/null +++ b/test/spec/api/v4/error-handling/carto-error-extender.spec.js @@ -0,0 +1,49 @@ +var getExtraFields = require('../../../../../src/api/v4/error-handling/carto-error-extender').getExtraFields; + +describe('api/v4/error-handling/carto-error-extender', function () { + it('extending an error with no entry in the error list should return default values', function () { + var extendedError = getExtraFields({}); + + expect(extendedError.friendlyMessage).toEqual(''); + expect(extendedError.errorCode).toEqual('unknown-error'); + }); + + it('extending an error with an entry in the error list that needs a regex replacement should return proper code and friendly message', function () { + var error = { + origin: 'windshaft', + type: 'analysis', + message: 'relation "invalid_value" does not exist' + }; + + var extendedError = getExtraFields(error); + + expect(extendedError.friendlyMessage).toEqual('Invalid dataset name used. Dataset "invalid_value" does not exist.'); + expect(extendedError.errorCode).toEqual('windshaft:analysis:invalid-dataset'); + }); + + it('extending an error with an entry that needs two regex replacements ($0 and $1) should return proper code and friendly message', function () { + var error = { + origin: 'validation', + type: 'layer', + message: 'wrongInteractivityColumns[column1, column2]#featureClick' + }; + + var extendedError = getExtraFields(error); + + expect(extendedError.friendlyMessage).toEqual('Columns [column1, column2] set on `featureClick` do not match the columns set in aggregation options.'); + expect(extendedError.errorCode).toEqual('validation:layer:wrong-interactivity-columns'); + }); + + it('extending an error with an entry in the error list that does not have friendly message should return proper code and original message', function () { + var error = { + origin: 'windshaft', + type: 'analysis', + message: 'syntax error: thruster is not a valid SQL word' + }; + + var extendedError = getExtraFields(error); + + expect(extendedError.friendlyMessage).toEqual('syntax error: thruster is not a valid SQL word'); + expect(extendedError.errorCode).toEqual('windshaft:analysis:sql-syntax-error'); + }); +}); diff --git a/test/spec/api/v4/error-handling/carto-error.spec.js b/test/spec/api/v4/error-handling/carto-error.spec.js new file mode 100644 index 0000000..8dfbfd7 --- /dev/null +++ b/test/spec/api/v4/error-handling/carto-error.spec.js @@ -0,0 +1,107 @@ +var CartoError = require('../../../../../src/api/v4/error-handling/carto-error'); + +describe('v4/error-handling/carto-error', function () { + it('should return default values if the error is not qualified', function () { + var cartoError = new CartoError({ + someProperty: 'some value' + }); + + expect(cartoError instanceof Error).toBe(true); + expect(cartoError.name).toEqual('CartoError'); + expect(cartoError.message).toEqual('unexpected error'); + expect(cartoError.origin).toEqual('generic'); + expect(cartoError.type).toEqual(''); + expect(cartoError.errorCode).toEqual('generic:unknown-error'); + expect(cartoError.originalError).toEqual(jasmine.objectContaining({ + someProperty: 'some value' + })); + }); + + describe('windshaft error', function () { + it('should return the original values and proper friendly message if it is a windshaft error', function () { + var cartoError = new CartoError({ + origin: 'windshaft', + type: 'layer', + message: 'column "jonica" does not exist' + }); + + expect(cartoError instanceof Error).toBe(true); + expect(cartoError.name).toEqual('CartoError'); + expect(cartoError.message).toEqual('Invalid column name. Column "jonica" does not exist.'); + expect(cartoError.origin).toEqual('windshaft'); + expect(cartoError.type).toEqual('layer'); + expect(cartoError.errorCode).toEqual('windshaft:layer:column-does-not-exist'); + expect(cartoError.originalError).toEqual(jasmine.objectContaining({ + origin: 'windshaft', + type: 'layer', + message: 'column "jonica" does not exist' + })); + }); + + it('should return the source id if it is a windshaft analysis error and analysis is provided', function () { + var cartoError = new CartoError({ + origin: 'windshaft', + type: 'analysis', + message: 'column "jonica" does not exist' + }, { + analysis: { + aKey: 'a value', + getId: function () { return 'S1'; } + } + }); + + expect(cartoError instanceof Error).toBe(true); + expect(cartoError.source).toBeDefined(); + expect(cartoError.source.aKey).toEqual('a value'); + expect(cartoError.sourceId).toEqual('S1'); + }); + + it('should return the source id if the windshaft analysis error has that value', function () { + var cartoError = new CartoError({ + origin: 'windshaft', + type: 'analysis', + message: 'column "jonica" does not exist', + analysisId: 'A1' + }); + + expect(cartoError instanceof Error).toBe(true); + expect(cartoError.sourceId).toEqual('A1'); + }); + }); + + it('should parse ajax error if original error is an ajax response', function () { + var cartoError = new CartoError({ + responseText: '{ "errors": ["an error"] }', + statusText: '404' + }); + + expect(cartoError instanceof Error).toBe(true); + expect(cartoError.name).toEqual('CartoError'); + expect(cartoError.message).toEqual('an error'); + expect(cartoError.origin).toEqual('ajax'); + expect(cartoError.type).toEqual('404'); + expect(cartoError.errorCode).toEqual('ajax:404:unknown-error'); + expect(cartoError.originalError).toEqual(jasmine.objectContaining({ + responseText: '{ "errors": ["an error"] }', + statusText: '404' + })); + }); + + /** + * There's a bug in PhantomJS that don't fill the stack property + * of an error. We need to assert that the `stack` property gets + * defined so we restrict the spec to run in the browser. + */ + it('should create an error with a stack trace', function () { + var cartoError = new CartoError({ + someProperty: 'some value' + }); + var userAgent = navigator.userAgent; + + if (userAgent.toLowerCase().indexOf('phantomjs') < 0) { + expect(cartoError.stack).toBeDefined(); + } else { + expect(true).toBe(true); + } + }); +}); diff --git a/test/spec/api/v4/error-handling/carto-validation-error.spec.js b/test/spec/api/v4/error-handling/carto-validation-error.spec.js new file mode 100644 index 0000000..69432d1 --- /dev/null +++ b/test/spec/api/v4/error-handling/carto-validation-error.spec.js @@ -0,0 +1,12 @@ +var CartoValidationError = require('../../../../../src/api/v4/error-handling/carto-validation-error'); + +describe('v4/error-handling/carto-validation-error', function () { + it('should return a CartoError with validation as type', function () { + var validationError = new CartoValidationError('layer', 'a message'); + + expect(validationError.name).toEqual('CartoError'); + expect(validationError.origin).toEqual('validation'); + expect(validationError.type).toEqual('layer'); + expect(validationError.message).toEqual('a message'); + }); +}); diff --git a/test/spec/api/v4/filter/base-sql.spec.js b/test/spec/api/v4/filter/base-sql.spec.js new file mode 100644 index 0000000..c525834 --- /dev/null +++ b/test/spec/api/v4/filter/base-sql.spec.js @@ -0,0 +1,250 @@ +const SQLBase = require('../../../../../src/api/v4/filter/base-sql'); + +const PARAMETER_SPECIFICATION = { + in: { parameters: [{ name: 'in', allowedTypes: ['Array', 'String'] }] }, + notIn: { parameters: [{ name: 'notIn', allowedTypes: ['Array', 'String'] }] }, + like: { parameters: [{ name: 'like', allowedTypes: ['Array', 'String'] }] } +}; + +const SQL_TEMPLATES = { + 'in': '<%= column %> IN (<%= value %>)', + 'like': '<%= column %> LIKE <%= value %>' +}; + +const column = 'fake_column'; + +describe('api/v4/filter/base-sql', function () { + describe('constructor', function () { + it('should throw a descriptive error when column is undefined, not a string, or empty', function () { + expect(function () { + new SQLBase(undefined); // eslint-disable-line + }).toThrowError('Column property is required.'); + + expect(function () { + new SQLBase(1); // eslint-disable-line + }).toThrowError('Column property must be a string.'); + + expect(function () { + new SQLBase(''); // eslint-disable-line + }).toThrowError('Column property must be not empty.'); + }); + + it('should throw a descriptive error when there is an invalid option', function () { + expect(function () { + new SQLBase('fake_column', { unknown_option: false }); // eslint-disable-line + }).toThrowError("'unknown_option' is not a valid option for this filter."); + }); + + it('should set column and options as class properties', function () { + const options = { includeNull: true }; + + const sqlFilter = new SQLBase(column, options); + + expect(sqlFilter._column).toBe(column); + expect(sqlFilter._options).toBe(options); + }); + }); + + describe('.set', function () { + it('should throw a descriptive error when an unknown filter has been passed', function () { + const sqlFilter = new SQLBase('fake_column'); + + expect(function () { + sqlFilter.set('unknown_filter', 'test_filter'); + }).toThrowError("'unknown_filter' is not a valid filter. Please check documentation."); + }); + + it('should set the new filter to the filters object', function () { + const sqlFilter = new SQLBase(column); + sqlFilter.ALLOWED_FILTERS = ['in']; + sqlFilter.PARAMETER_SPECIFICATION = { in: PARAMETER_SPECIFICATION.in }; + + sqlFilter.set('in', ['test_filter']); + + expect(sqlFilter._filters).toEqual({ in: ['test_filter'] }); + }); + + it("should trigger a 'change:filters' event", function () { + const spy = jasmine.createSpy(); + + const sqlFilter = new SQLBase('fake_column'); + sqlFilter.ALLOWED_FILTERS = ['in']; + sqlFilter.PARAMETER_SPECIFICATION = { in: PARAMETER_SPECIFICATION.in }; + sqlFilter.on('change:filters', spy); + + sqlFilter.set('in', ['test_filter']); + + expect(spy).toHaveBeenCalled(); + }); + }); + + describe('.setFilters', function () { + it('should throw a descriptive error when an unknown filter has been passed', function () { + const sqlFilter = new SQLBase('fake_column'); + + expect(function () { + sqlFilter.setFilters({ unknown_filter: 'test_filter' }); + }).toThrowError("'unknown_filter' is not a valid filter. Please check documentation."); + }); + + it('should set the new filters and override previous ones', function () { + const newFilters = { notIn: 'test_filter2' }; + + const sqlFilter = new SQLBase(column); + sqlFilter.ALLOWED_FILTERS = ['in', 'notIn']; + sqlFilter.PARAMETER_SPECIFICATION = { + in: PARAMETER_SPECIFICATION.in, + notIn: PARAMETER_SPECIFICATION.notIn + }; + sqlFilter.set('in', ['test_filter']); + + sqlFilter.setFilters(newFilters); + + expect(sqlFilter._filters).toEqual(newFilters); + }); + + it("should trigger a 'change:filters' event", function () { + const newFilters = { notIn: 'test_filter2' }; + const spy = jasmine.createSpy(); + + const sqlFilter = new SQLBase(column); + sqlFilter.ALLOWED_FILTERS = ['in', 'notIn']; + sqlFilter.PARAMETER_SPECIFICATION = { + in: PARAMETER_SPECIFICATION.in, + notIn: PARAMETER_SPECIFICATION.notIn + }; + sqlFilter.set('in', ['test_filter']); + sqlFilter.on('change:filters', spy); + + sqlFilter.setFilters(newFilters); + + expect(spy).toHaveBeenCalledTimes(1); + }); + }); + + describe('.resetFilters', function () { + it('should reset applied filters', function () { + const sqlFilter = new SQLBase(column); + sqlFilter.ALLOWED_FILTERS = ['in']; + sqlFilter.PARAMETER_SPECIFICATION = { in: PARAMETER_SPECIFICATION.in }; + sqlFilter.set('in', ['test_filter']); + + sqlFilter.resetFilters(); + + expect(sqlFilter._filters).toEqual({}); + }); + }); + + describe('.$getSQL', function () { + it('should return SQL string containing all the filters joined by AND clause', function () { + const sqlFilter = new SQLBase(column); + sqlFilter.ALLOWED_FILTERS = ['in', 'like']; + sqlFilter.PARAMETER_SPECIFICATION = { + in: PARAMETER_SPECIFICATION.in, + like: PARAMETER_SPECIFICATION.like + }; + sqlFilter.SQL_TEMPLATES = { + in: SQL_TEMPLATES.in, + like: SQL_TEMPLATES.like + }; + sqlFilter.setFilters({ in: ['category 1', 'category 2'], like: '%category%' }); + + expect(sqlFilter.$getSQL()).toBe("(fake_column IN ('category 1','category 2') AND fake_column LIKE '%category%')"); + }); + + it('should call _includeNullInQuery if includeNull option is set', function () { + const sqlFilter = new SQLBase(column, { includeNull: true }); + sqlFilter.ALLOWED_FILTERS = ['in']; + sqlFilter.PARAMETER_SPECIFICATION = { in: PARAMETER_SPECIFICATION.in }; + sqlFilter.SQL_TEMPLATES = { in: SQL_TEMPLATES.in }; + sqlFilter.setFilters({ in: ['category 1', 'category 2'] }); + + spyOn(sqlFilter, '_includeNullInQuery'); + + sqlFilter.$getSQL(); + + expect(sqlFilter._includeNullInQuery).toHaveBeenCalled(); + }); + }); + + describe('.checkFilters', function () { + let sqlFilter; + + beforeEach(function () { + sqlFilter = new SQLBase(column); + }); + + it('should throw an error when an invalid filter is passed', function () { + expect(function () { + sqlFilter._checkFilters({ unknown_filter: 'filter' }); + }).toThrowError("'unknown_filter' is not a valid filter. Please check documentation."); + }); + + it("should throw an error when there's a type mismatching in filter parameters", function () { + expect(function () { + sqlFilter.ALLOWED_FILTERS = ['in']; + sqlFilter.PARAMETER_SPECIFICATION = { in: PARAMETER_SPECIFICATION.in }; + + sqlFilter._checkFilters({ in: 1 }); + }).toThrowError("Invalid parameter type for 'in'. Please check filters documentation."); + }); + }); + + describe('._convertValueToSQLString', function () { + it('should format date to ISO8601 string', function () { + const sqlFilter = new SQLBase(column); + + const fakeDate = new Date('Thu Jun 28 2018 15:04:31 GMT+0200 (Central European Summer Time)'); + expect(sqlFilter._convertValueToSQLString(fakeDate)).toBe('\'2018-06-28T13:04:31.000Z\''); + }); + + it('should convert array to a comma-separated string wrapped by single comma', function () { + const sqlFilter = new SQLBase(column); + + const fakeArray = ['Element 1', 'Element 2']; + expect(sqlFilter._convertValueToSQLString(fakeArray)).toBe("'Element 1','Element 2'"); + }); + + it('should return number without modifying', function () { + const sqlFilter = new SQLBase(column); + + expect(sqlFilter._convertValueToSQLString(1)).toBe(1); + }); + + it('should return object with string values without modifying', function () { + const sqlFilter = new SQLBase(column); + + const fakeObject = { fakeProperty: 'fakeValue' }; + + expect(sqlFilter._convertValueToSQLString(fakeObject)).toEqual(fakeObject); + }); + + it('should return object with date values parsed properly', function () { + const sqlFilter = new SQLBase(column); + + const fakeObject = { fakeDate: new Date('2014-01-01T00:00:00.00Z') }; + + expect(sqlFilter._convertValueToSQLString(fakeObject)).toEqual({ fakeDate: '\'2014-01-01T00:00:00.000Z\'' }); + }); + + it('should wrap strings in single-quotes', function () { + const sqlFilter = new SQLBase(column); + + const fakeString = 'fake_string'; + + expect(sqlFilter._convertValueToSQLString(fakeString)).toBe(`'${fakeString}'`); + }); + }); + + describe('._interpolateFilterIntoTemplate', function () { + it('should inject filter values into SQL template', function () { + const sqlFilter = new SQLBase(column); + + sqlFilter.SQL_TEMPLATES = { + gte: '<%= column %> > <%= value %>' + }; + + expect(sqlFilter._interpolateFilter('gte', 10)).toBe('fake_column > 10'); + }); + }); +}); diff --git a/test/spec/api/v4/filter/bounding-box-gmaps.spec.js b/test/spec/api/v4/filter/bounding-box-gmaps.spec.js new file mode 100644 index 0000000..4172692 --- /dev/null +++ b/test/spec/api/v4/filter/bounding-box-gmaps.spec.js @@ -0,0 +1,11 @@ +var carto = require('../../../../../src/api/v4/index'); + +describe('api/v4/filter/bounding-box-gmaps', function () { + describe('constructor', function () { + it('should throw a descriptive error when initialized with invalid parameters', function () { + expect(function () { + new carto.filter.BoundingBoxGoogleMaps(undefined); // eslint-disable-line + }).toThrowError('Bounding box requires a Google Maps map but got: undefined'); + }); + }); +}); diff --git a/test/spec/api/v4/filter/bounding-box-leaflet.spec.js b/test/spec/api/v4/filter/bounding-box-leaflet.spec.js new file mode 100644 index 0000000..6666df6 --- /dev/null +++ b/test/spec/api/v4/filter/bounding-box-leaflet.spec.js @@ -0,0 +1,11 @@ +var carto = require('../../../../../src/api/v4/index'); + +describe('api/v4/filter/bounding-box-leaflet', function () { + describe('constructor', function () { + it('should throw a descriptive error when initialized with invalid parameters', function () { + expect(function () { + new carto.filter.BoundingBoxLeaflet(undefined); // eslint-disable-line + }).toThrowError('Bounding box requires a Leaflet map but got: undefined'); + }); + }); +}); diff --git a/test/spec/api/v4/filter/bounding-box.spec.js b/test/spec/api/v4/filter/bounding-box.spec.js new file mode 100644 index 0000000..af79a6a --- /dev/null +++ b/test/spec/api/v4/filter/bounding-box.spec.js @@ -0,0 +1,50 @@ +var carto = require('../../../../../src/api/v4/index'); + +describe('api/v4/filter/bounding-box', function () { + describe('initialization', function () { + it('should create the internalModel', function () { + var bboxFilter = new carto.filter.BoundingBox(); + + expect(bboxFilter.$getInternalModel()).toBeDefined(); + }); + }); + + describe('.setBounds', function () { + var bboxFilter; + + beforeEach(function () { + bboxFilter = new carto.filter.BoundingBox(); + }); + + it('checks if bounds are valid', function () { + var test = function () { + bboxFilter.setBounds({ west: 0 }); + }; + + expect(test).toThrowError(Error, 'Bounds object is not valid. Use a carto.filter.Bounds object'); + }); + + it('if bounds are valid, it assigns it to property, triggers the boundsChanged event and returns this', function () { + spyOn(bboxFilter, 'trigger'); + var bounds = { west: 1, south: 2, east: 3, north: 4 }; + var returnedObject = bboxFilter.setBounds(bounds); + + expect(bboxFilter.getBounds()).toEqual(bounds); + expect(bboxFilter.trigger).toHaveBeenCalledWith('boundsChanged', bounds); + expect(returnedObject).toBe(bboxFilter); + }); + }); + + describe('.resetBounds', function () { + it('sets the bounds to 0,0,0,0', function () { + var bboxFilter = new carto.filter.BoundingBox(); + bboxFilter.setBounds({ west: 1, south: 2, east: 3, north: 4 }); + + expect(bboxFilter.getBounds()).toEqual({ west: 1, south: 2, east: 3, north: 4 }); + + bboxFilter.resetBounds(); + + expect(bboxFilter.getBounds()).toEqual({ west: 0, south: 0, east: 0, north: 0 }); + }); + }); +}); diff --git a/test/spec/api/v4/filter/category.spec.js b/test/spec/api/v4/filter/category.spec.js new file mode 100644 index 0000000..d376fdb --- /dev/null +++ b/test/spec/api/v4/filter/category.spec.js @@ -0,0 +1,63 @@ +const carto = require('../../../../../src/api/v4/index'); + +describe('api/v4/filter/category', function () { + describe('constructor', function () { + it('should throw a descriptive error when an unknown filter has been passed', function () { + expect(function () { + new carto.filter.Category('fake_column', { unknown_filter: '' }); // eslint-disable-line + }).toThrowError("'unknown_filter' is not a valid filter. Please check documentation."); + }); + }); + + describe('SQL Templates', function () { + it('IN', function () { + const categoryFilter = new carto.filter.Category('fake_column', { in: ['Category 1'] }); + expect(categoryFilter.$getSQL()).toBe("fake_column IN ('Category 1')"); + }); + + it('IN with subquery', function () { + const categoryFilter = new carto.filter.Category('fake_column', { in: { query: 'SELECT name FROM neighbourhoods' } }); + expect(categoryFilter.$getSQL()).toBe('fake_column IN (SELECT name FROM neighbourhoods)'); + }); + + it('NOT IN', function () { + const categoryFilter = new carto.filter.Category('fake_column', { notIn: ['Category 1'] }); + expect(categoryFilter.$getSQL()).toBe("fake_column NOT IN ('Category 1')"); + }); + + it('NOT IN with subquery', function () { + const categoryFilter = new carto.filter.Category('fake_column', { notIn: { query: 'SELECT name FROM neighbourhoods' } }); + expect(categoryFilter.$getSQL()).toBe('fake_column NOT IN (SELECT name FROM neighbourhoods)'); + }); + + it('EQ', function () { + const categoryFilter = new carto.filter.Category('fake_column', { eq: 'Category 1' }); + expect(categoryFilter.$getSQL()).toBe("fake_column = 'Category 1'"); + }); + + it('EQ with subquery', function () { + const categoryFilter = new carto.filter.Category('fake_column', { eq: { query: 'SELECT avg(price) FROM neighbourhoods' } }); + expect(categoryFilter.$getSQL()).toBe('fake_column = (SELECT avg(price) FROM neighbourhoods)'); + }); + + it('NOT EQ', function () { + const categoryFilter = new carto.filter.Category('fake_column', { notEq: 'Category 1' }); + expect(categoryFilter.$getSQL()).toBe("fake_column != 'Category 1'"); + }); + + it('NOT EQ with subquery', function () { + const categoryFilter = new carto.filter.Category('fake_column', { notEq: { query: 'SELECT avg(price) FROM neighbourhoods' } }); + expect(categoryFilter.$getSQL()).toBe('fake_column != (SELECT avg(price) FROM neighbourhoods)'); + }); + + it('LIKE', function () { + const categoryFilter = new carto.filter.Category('fake_column', { like: '%Category%' }); + expect(categoryFilter.$getSQL()).toBe("fake_column LIKE '%Category%'"); + }); + + it('SIMILAR TO', function () { + const categoryFilter = new carto.filter.Category('fake_column', { similarTo: '%Category%' }); + expect(categoryFilter.$getSQL()).toBe("fake_column SIMILAR TO '%Category%'"); + }); + }); +}); diff --git a/test/spec/api/v4/filter/circle.spec.js b/test/spec/api/v4/filter/circle.spec.js new file mode 100644 index 0000000..70529a7 --- /dev/null +++ b/test/spec/api/v4/filter/circle.spec.js @@ -0,0 +1,56 @@ +var carto = require('../../../../../src/api/v4/index'); + +describe('api/v4/filter/circle', function () { + describe('initialization', function () { + it('should create the internalModel', function () { + var circleFilter = new carto.filter.Circle(); + + expect(circleFilter.$getInternalModel()).toBeDefined(); + }); + }); + + describe('.setCircle', function () { + var circleFilter; + + beforeEach(function () { + circleFilter = new carto.filter.Circle(); + }); + + it('checks if circle is valid', function () { + var test = function () { + circleFilter.setCircle({ lng: 0 }); + }; + + var test2 = function () { + circleFilter.setCircle({ lng: 'a' }); + }; + + const expected = 'Circle object is not valid. Use a carto.filter.CircleData object'; + expect(test).toThrowError(Error, expected); + expect(test2).toThrowError(Error, expected); + }); + + it('if circle is valid, it assigns it to property, triggers the circleChanged event and returns this', function () { + spyOn(circleFilter, 'trigger'); + var circle = { lat: 1, lng: 2, radius: 3 }; + var returnedObject = circleFilter.setCircle(circle); + + expect(circleFilter.getCircle()).toEqual(circle); + expect(circleFilter.trigger).toHaveBeenCalledWith('circleChanged', circle); + expect(returnedObject).toBe(circleFilter); + }); + }); + + describe('.resetCircle', function () { + it('sets the circle to 0,0,0', function () { + var circleFilter = new carto.filter.Circle(); + circleFilter.setCircle({ lat: 1, lng: 2, radius: 3 }); + + expect(circleFilter.getCircle()).toEqual({ lat: 1, lng: 2, radius: 3 }); + + circleFilter.resetCircle(); + + expect(circleFilter.getCircle()).toEqual({ lat: 0, lng: 0, radius: 0 }); + }); + }); +}); diff --git a/test/spec/api/v4/filter/filters-collection.spec.js b/test/spec/api/v4/filter/filters-collection.spec.js new file mode 100644 index 0000000..176ecdc --- /dev/null +++ b/test/spec/api/v4/filter/filters-collection.spec.js @@ -0,0 +1,174 @@ +const FiltersCollection = require('../../../../../src/api/v4/filter/filters-collection'); +const carto = require('../../../../../src/api/v4/index'); + +const column = 'fake_column'; + +describe('api/v4/filter/filters-collection', function () { + describe('constructor', function () { + it('should call _initialize', function () { + spyOn(FiltersCollection.prototype, '_initialize'); + new FiltersCollection(); // eslint-disable-line + + expect(FiltersCollection.prototype._initialize).toHaveBeenCalled(); + }); + }); + + describe('._initialize', function () { + it('should set an empty array to filters', function () { + const filtersCollection = new FiltersCollection(); + expect(filtersCollection._filters).toEqual([]); + }); + + it('should set provided filters and call add()', function () { + spyOn(FiltersCollection.prototype, 'addFilter').and.callThrough(); + + const filters = [ + new carto.filter.Range(column, { lt: 1 }), + new carto.filter.Category(column, { in: ['category'] }) + ]; + const filtersCollection = new FiltersCollection(filters); + + expect(filtersCollection._filters).toEqual(filters); + expect(FiltersCollection.prototype.addFilter).toHaveBeenCalledTimes(2); + }); + }); + + describe('.addFilter', function () { + let filtersCollection, rangeFilter, triggerFilterChangeSpy, listenToChangeSpy; + + beforeEach(function () { + triggerFilterChangeSpy = spyOn(FiltersCollection.prototype, '_triggerFilterChange'); + listenToChangeSpy = spyOn(FiltersCollection.prototype, 'listenTo'); + filtersCollection = new FiltersCollection(); + rangeFilter = new carto.filter.Range(column, { lt: 1 }); + }); + + it('should throw an error if filter is not an instance of SQLBase or FiltersCollection', function () { + expect(function () { + filtersCollection.addFilter({}); + }).toThrowError('Filters need to extend from carto.filter.SQLBase. Please use carto.filter.Category or carto.filter.Range.'); + }); + + it('should not readd a filter if it is already added', function () { + filtersCollection.addFilter(rangeFilter); + filtersCollection.addFilter(rangeFilter); + + expect(filtersCollection._filters.length).toBe(1); + expect(triggerFilterChangeSpy).toHaveBeenCalledTimes(1); + }); + + it('should add new filter and trigger change:filters event', function () { + filtersCollection.addFilter(rangeFilter); + + expect(filtersCollection._filters.length).toBe(1); + expect(triggerFilterChangeSpy).toHaveBeenCalledTimes(1); + }); + + it('should register listener to change:filters event in the added filter', function () { + filtersCollection.addFilter(rangeFilter); + + expect(listenToChangeSpy).toHaveBeenCalled(); + expect(listenToChangeSpy.calls.mostRecent().args[0]).toBe(rangeFilter); + expect(listenToChangeSpy.calls.mostRecent().args[1]).toEqual('change:filters'); + }); + }); + + describe('.removeFilter', function () { + let filtersCollection, rangeFilter, triggerFilterChangeSpy; + + beforeEach(function () { + triggerFilterChangeSpy = spyOn(FiltersCollection.prototype, '_triggerFilterChange'); + filtersCollection = new FiltersCollection(); + rangeFilter = new carto.filter.Range(column, { lt: 1 }); + }); + + it('should not remove the filter if it was not already added', function () { + const removedElement = filtersCollection.removeFilter(rangeFilter); + + expect(removedElement).toBeUndefined(); + expect(triggerFilterChangeSpy).not.toHaveBeenCalled(); + }); + + it('should remove the filter if it was already added', function () { + filtersCollection.addFilter(rangeFilter); + + const removedElement = filtersCollection.removeFilter(rangeFilter); + + expect(removedElement).toBe(rangeFilter); + expect(triggerFilterChangeSpy).toHaveBeenCalled(); + }); + }); + + describe('.count', function () { + let filtersCollection, rangeFilter; + + beforeEach(function () { + filtersCollection = new FiltersCollection(); + rangeFilter = new carto.filter.Range(column, { lt: 1 }); + filtersCollection.addFilter(rangeFilter); + }); + + it('should return filters length', function () { + expect(filtersCollection.count()).toBe(1); + }); + }); + + describe('.getFilters', function () { + let filtersCollection, rangeFilter; + + beforeEach(function () { + filtersCollection = new FiltersCollection(); + rangeFilter = new carto.filter.Range(column, { lt: 1 }); + filtersCollection.addFilter(rangeFilter); + }); + + it('should return added filters', function () { + expect(filtersCollection.getFilters()).toEqual([rangeFilter]); + }); + }); + + describe('.$getSQL', function () { + let filtersCollection; + + beforeEach(function () { + let rangeFilter = new carto.filter.Range(column, { lt: 1 }); + let categoryFilter = new carto.filter.Category(column, { in: ['category'] }); + + filtersCollection = new FiltersCollection(); + filtersCollection.addFilter(rangeFilter); + filtersCollection.addFilter(categoryFilter); + }); + + it('should build the SQL string and join filters', function () { + expect(filtersCollection.$getSQL()).toEqual("(fake_column < 1 AND fake_column IN ('category'))"); + }); + + it('should not take empty filters into account', function () { + let customRangeFilter = new carto.filter.Range(column, { lt: 1 }); + let emptyFilter = new carto.filter.Category(column, {}); + + filtersCollection = new FiltersCollection(); + filtersCollection.addFilter(customRangeFilter); + filtersCollection.addFilter(emptyFilter); + + expect(filtersCollection.$getSQL()).toEqual('fake_column < 1'); + }); + }); + + describe('._triggerFilterChange', function () { + let filtersCollection; + + beforeEach(function () { + filtersCollection = new FiltersCollection(); + }); + + it('should trigger change:filters', function () { + spyOn(filtersCollection, 'trigger'); + + const filters = []; + filtersCollection._triggerFilterChange(filters); + + expect(filtersCollection.trigger).toHaveBeenCalledWith('change:filters', filters); + }); + }); +}); diff --git a/test/spec/api/v4/filter/polygon.spec.js b/test/spec/api/v4/filter/polygon.spec.js new file mode 100644 index 0000000..1fbb903 --- /dev/null +++ b/test/spec/api/v4/filter/polygon.spec.js @@ -0,0 +1,75 @@ +var carto = require('../../../../../src/api/v4/index'); + +describe('api/v4/filter/polygon', function () { + describe('initialization', function () { + it('should create the internalModel', function () { + var polygonFilter = new carto.filter.Polygon(); + + expect(polygonFilter.$getInternalModel()).toBeDefined(); + }); + }); + + describe('.setPolygon', function () { + var polygonFilter; + + beforeEach(function () { + polygonFilter = new carto.filter.Polygon(); + }); + + it('checks if polygon is valid', function () { + var test = function () { + polygonFilter.setPolygon({ + ups: 'notExpected' + }); + }; + + var test2 = function () { + polygonFilter.setPolygon({ + type: 'NotAPolygon', + coordinates: { + content: 'isNotAnArrayOfCoords' + } + }); + }; + + const expected = 'Polygon object is not valid. Use a carto.filter.PolygonData object'; + expect(test).toThrowError(Error, expected); + expect(test2).toThrowError(Error, expected); + }); + + it('if polygon is valid, it assigns it to property, triggers the polygonChanged event and returns this', function () { + spyOn(polygonFilter, 'trigger'); + var polygon = { + type: 'Polygon', + coordinates: [[1, 2], [3, 4], [5, 6], [1, 2]] + }; + var returnedObject = polygonFilter.setPolygon(polygon); + + expect(polygonFilter.getPolygon()).toEqual(polygon); + expect(polygonFilter.trigger).toHaveBeenCalledWith('polygonChanged', polygon); + expect(returnedObject).toBe(polygonFilter); + }); + }); + + describe('.resetPolygon', function () { + it('sets the polygon as empty', function () { + var polygonFilter = new carto.filter.Polygon(); + polygonFilter.setPolygon({ + type: 'Polygon', + coordinates: [[1, 2], [3, 4], [5, 6], [1, 2]] + }); + + expect(polygonFilter.getPolygon()).toEqual({ + type: 'Polygon', + coordinates: [[1, 2], [3, 4], [5, 6], [1, 2]] + }); + + polygonFilter.resetPolygon(); + + expect(polygonFilter.getPolygon()).toEqual({ + type: 'Polygon', + coordinates: [] + }); + }); + }); +}); diff --git a/test/spec/api/v4/filter/range.spec.js b/test/spec/api/v4/filter/range.spec.js new file mode 100644 index 0000000..2f54a38 --- /dev/null +++ b/test/spec/api/v4/filter/range.spec.js @@ -0,0 +1,73 @@ +const carto = require('../../../../../src/api/v4/index'); + +describe('api/v4/filter/range', function () { + describe('constructor', function () { + it('should throw a descriptive error when an unknown filter has been passed', function () { + expect(function () { + new carto.filter.Range('fake_column', { unknown_filter: '' }); // eslint-disable-line + }).toThrowError("'unknown_filter' is not a valid filter. Please check documentation."); + }); + }); + + describe('SQL Templates', function () { + it('LT', function () { + const categoryFilter = new carto.filter.Range('fake_column', { lt: 10 }); + expect(categoryFilter.$getSQL()).toBe('fake_column < 10'); + }); + + it('LT with subquery', function () { + const categoryFilter = new carto.filter.Range('fake_column', { lt: { query: 'SELECT avg(price) FROM neighbourhoods' } }); + expect(categoryFilter.$getSQL()).toBe('fake_column < (SELECT avg(price) FROM neighbourhoods)'); + }); + + it('LTE', function () { + const categoryFilter = new carto.filter.Range('fake_column', { lte: 10 }); + expect(categoryFilter.$getSQL()).toBe('fake_column <= 10'); + }); + + it('LTE with subquery', function () { + const categoryFilter = new carto.filter.Range('fake_column', { lte: { query: 'SELECT avg(price) FROM neighbourhoods' } }); + expect(categoryFilter.$getSQL()).toBe('fake_column <= (SELECT avg(price) FROM neighbourhoods)'); + }); + + it('GT', function () { + const categoryFilter = new carto.filter.Range('fake_column', { gt: 10 }); + expect(categoryFilter.$getSQL()).toBe('fake_column > 10'); + }); + + it('GT with subquery', function () { + const categoryFilter = new carto.filter.Range('fake_column', { gt: { query: 'SELECT avg(price) FROM neighbourhoods' } }); + expect(categoryFilter.$getSQL()).toBe('fake_column > (SELECT avg(price) FROM neighbourhoods)'); + }); + + it('GTE', function () { + const categoryFilter = new carto.filter.Range('fake_column', { gte: 10 }); + expect(categoryFilter.$getSQL()).toBe('fake_column >= 10'); + }); + + it('GTE with subquery', function () { + const categoryFilter = new carto.filter.Range('fake_column', { gte: { query: 'SELECT avg(price) FROM neighbourhoods' } }); + expect(categoryFilter.$getSQL()).toBe('fake_column >= (SELECT avg(price) FROM neighbourhoods)'); + }); + + it('BETWEEN', function () { + const categoryFilter = new carto.filter.Range('fake_column', { between: { min: 1, max: 10 } }); + expect(categoryFilter.$getSQL()).toBe('fake_column BETWEEN 1 AND 10'); + }); + + it('NOT BETWEEN', function () { + const categoryFilter = new carto.filter.Range('fake_column', { notBetween: { min: 1, max: 10 } }); + expect(categoryFilter.$getSQL()).toBe('fake_column NOT BETWEEN 1 AND 10'); + }); + + it('BETWEEN SYMMETRIC', function () { + const categoryFilter = new carto.filter.Range('fake_column', { betweenSymmetric: { min: 1, max: 10 } }); + expect(categoryFilter.$getSQL()).toBe('fake_column BETWEEN SYMMETRIC 1 AND 10'); + }); + + it('NOT BETWEEN SYMMETRIC', function () { + const categoryFilter = new carto.filter.Range('fake_column', { notBetweenSymmetric: { min: 1, max: 10 } }); + expect(categoryFilter.$getSQL()).toBe('fake_column NOT BETWEEN SYMMETRIC 1 AND 10'); + }); + }); +}); diff --git a/test/spec/api/v4/layer/aggregation.spec.js b/test/spec/api/v4/layer/aggregation.spec.js new file mode 100644 index 0000000..59beabf --- /dev/null +++ b/test/spec/api/v4/layer/aggregation.spec.js @@ -0,0 +1,171 @@ +var Aggregation = require('../../../../../src/api/v4/layer/aggregation'); + +describe('layer-aggregation', function () { + var options; + beforeEach(function () { + options = { + threshold: 10000, + resolution: 1, + placement: 'point-sample', + columns: { + fake_name_0: { + aggregateFunction: 'sum', + aggregatedColumn: 'fake_column_0' + }, + fake_name_1: { + aggregateFunction: 'avg', + aggregatedColumn: 'fake_column_1' + } + } + }; + }); + + describe('constructor', function () { + it('should return a simple object when the parameters are valid', function () { + var aggregation = new Aggregation(options); + + // Multiple specs for easy debugging + expect(aggregation.threshold).toEqual(options.threshold); + expect(aggregation.resolution).toEqual(options.resolution); + expect(aggregation.placement).toEqual(options.placement); + expect(aggregation.columns.fake_name_0).toEqual({ + aggregate_function: 'sum', + aggregated_column: 'fake_column_0' + }); + expect(aggregation.columns.fake_name_1).toEqual({ + aggregate_function: 'avg', + aggregated_column: 'fake_column_1' + }); + }); + + describe('errors', function () { + describe('threshold', function () { + it('should throw an error when threshold is not defined', function () { + delete options.threshold; + expect(function () { + new Aggregation(options); // eslint-disable-line + }).toThrowError('Aggregation threshold is required.'); + }); + + it('should throw an error when threshold is not a positive integer', function () { + options.threshold = 0; + expect(function () { + new Aggregation(options); // eslint-disable-line + }).toThrowError('Aggregation threshold must be an integer value greater than 0.'); + + options.threshold = -1; + expect(function () { + new Aggregation(options); // eslint-disable-line + }).toThrowError('Aggregation threshold must be an integer value greater than 0.'); + + options.threshold = 2.5; + expect(function () { + new Aggregation(options); // eslint-disable-line + }).toThrowError('Aggregation threshold must be an integer value greater than 0.'); + }); + }); + + describe('resolution', function () { + it('should throw an error when resolution is not defined', function () { + options.resolution = undefined; + expect(function () { + new Aggregation(options); // eslint-disable-line + }).toThrowError('Aggregation resolution is required.'); + }); + + it('should throw an error when resolution is not an integer between 1 and 16', function () { + var expectedErrorMessage = 'Aggregation resolution must be 0.5, 1 or powers of 2 up to 256 (2, 4, 8, 16, 32, 64, 128, 256).'; + options.resolution = 0; + expect(function () { + new Aggregation(options); // eslint-disable-line + }).toThrowError(expectedErrorMessage); + + options.resolution = 17; + expect(function () { + new Aggregation(options); // eslint-disable-line + }).toThrowError(expectedErrorMessage); + + var validOnes = [0.5, 1, 2, 4, 8, 16, 32, 64, 128, 256]; + validOnes.forEach(function (resolution) { + expect(function () { + options.resolution = resolution; + new Aggregation(options); // eslint-disable-line + }).not.toThrowError(); + }); + }); + }); + + describe('placement', function () { + it('should throw an error when placement is not one of our three valid placements', function () { + options.placement = 'invalid_placement'; + expect(function () { + new Aggregation(options); // eslint-disable-line + }).toThrowError('Aggregation placement is not valid. Must be one of these values: `point-sample`, `point-grid`, `centroid`'); + }); + }); + + describe('columns', function () { + it('should thrown an error when column.aggregateFunction is not defined', function () { + options.columns = { + fake_name_0: { + aggregatedColumn: 'fake_column_0' + } + }; + + expect(function () { + new Aggregation(options); // eslint-disable-line + }).toThrowError("Aggregation function for column 'fake_name_0' is required."); + }); + + it('should thrown an error when column.aggregateFunction is not a valid function', function () { + options.columns = { + fake_name_0: { + aggregatedColumn: 'fake_column_0', + aggregateFunction: 'invalid_function' + } + }; + + expect(function () { + new Aggregation(options); // eslint-disable-line + }).toThrowError("Aggregation function for column 'fake_name_0' is not valid. Use carto.aggregation.function"); + }); + + it('should thrown an error when column.aggregatedColumn is not defined', function () { + options.columns = { + fake_name_0: { + aggregateFunction: 'sum' + } + }; + + expect(function () { + new Aggregation(options); // eslint-disable-line + }).toThrowError("Column to be aggregated to 'fake_name_0' is required."); + }); + + it('should thrown an error when column.aggregatedColumn is not a string', function () { + options.columns = { + fake_name_0: { + aggregatedColumn: 4500, + aggregateFunction: 'sum' + } + }; + + expect(function () { + new Aggregation(options); // eslint-disable-line + }).toThrowError("Column to be aggregated to 'fake_name_0' must be a string."); + }); + }); + + describe('optional placement and columns', function () { + it('should now throw an error when neither placement nor columns appear', function () { + delete options.columns; + delete options.placement; + + expect(function () { + new Aggregation(options); // eslint-disable-line + }).not.toThrow(); + }); + }); + }); + }); +}); diff --git a/test/spec/api/v4/layer/layer.spec.js b/test/spec/api/v4/layer/layer.spec.js new file mode 100644 index 0000000..0983c3c --- /dev/null +++ b/test/spec/api/v4/layer/layer.spec.js @@ -0,0 +1,677 @@ +var carto = require('../../../../../src/api/v4'); + +describe('api/v4/layer', function () { + var source; + var style; + var originalTimeout; + + beforeEach(function () { + source = new carto.source.Dataset('ne_10m_populated_places_simple'); + style = new carto.style.CartoCSS('#layer { marker-fill: red; }'); + originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; + jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; + }); + + afterEach(function () { + jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; + }); + + describe('constructor', function () { + it('should build a new Layer params: (source, style)', function () { + var layer = new carto.layer.Layer(source, style); + + expect(layer.getSource()).toEqual(source); + expect(layer.getStyle()).toEqual(style); + }); + + it('should assign a unique layer ID string', function () { + var layer1 = new carto.layer.Layer(source, style); + var layer2 = new carto.layer.Layer(source, style); + + var id1 = layer1.getId(); + var id2 = layer2.getId(); + + expect(id1).toMatch(/L\d+/); + expect(id2).toMatch(/L\d+/); + expect(id1).not.toEqual(id2); + }); + + it('should be able to create a hidden layer', function () { + var layer = new carto.layer.Layer(source, style, { + visible: false + }); + + expect(layer.isHidden()).toBe(true); + }); + + it('should build a new Layer params: (source, style, options)', function () { + var layer = new carto.layer.Layer(source, style, { + featureClickColumns: ['a', 'b'], + featureOverColumns: ['c', 'd'] + }); + + expect(layer.getSource()).toEqual(source); + expect(layer.getStyle()).toEqual(style); + expect(layer.getFeatureClickColumns()).toEqual(['a', 'b']); + expect(layer.getFeatureOverColumns()).toEqual(['c', 'd']); + }); + + it('should throw an error if source is not valid', function () { + expect(function () { + new carto.layer.Layer({}, style); // eslint-disable-line + }).toThrowError('The given object is not a valid source. See "carto.source.Base".'); + }); + + it('should throw an error if style is not valid', function () { + expect(function () { + new carto.layer.Layer(source, {}); // eslint-disable-line + }).toThrowError('The given object is not a valid style. See "carto.style.Base".'); + }); + + it('should allow custom layer id as an option', function () { + var layer = new carto.layer.Layer(source, style, { id: 'fake_id' }); + expect(layer.getId()).toEqual('fake_id'); + }); + + describe('columns validation', function () { + var aggregation = new carto.layer.Aggregation({ + threshold: 1, + resolution: 4, + columns: { + population: { + aggregateFunction: 'sum', + aggregatedColumn: 'pop_max' + } + } + }); + + it('should validate that featureClick columns are contained in aggregation columns', function () { + expect(function () { + new carto.layer.Layer(source, style, { // eslint-disable-line + featureClickColumns: ['a', 'b'], + aggregation: aggregation + }); + }).toThrowError('Columns [a, b] set on `featureClick` do not match the columns set in aggregation options.'); + }); + + it('should validate that featureOver columns are contained in aggregation columns', function () { + expect(function () { + new carto.layer.Layer(source, style, { // eslint-disable-line + featureOverColumns: ['a', 'b'], + aggregation: aggregation + }); + }).toThrowError('Columns [a, b] set on `featureOver` do not match the columns set in aggregation options.'); + }); + }); + }); + + describe('.setStyle', function () { + var layer; + var newStyle; + beforeEach(function () { + layer = new carto.layer.Layer(source, style); + newStyle = new carto.style.CartoCSS('#layer { marker-fill: green; }'); + }); + + it('should throw an error when the parameter is not a valid style', function () { + expect(function () { + layer.setStyle('bad-style'); + }).toThrowError('The given object is not a valid style. See "carto.style.Base".'); + }); + + describe('when the layer has no engine', function () { + it('should set the layer style', function () { + layer.setStyle(newStyle); + + expect(layer.getStyle()).toEqual(newStyle); + }); + + it('should fire a styleChanged event', function (done) { + layer.on('styleChanged', function (l) { + expect(l).toBe(layer); + expect(l.getStyle()).toEqual(newStyle); + done(); + }); + + layer.setStyle(newStyle); + }); + + it('should not fire a styleChanged event when setting the same style twice', function () { + var styleChangedSpy = jasmine.createSpy('styleChangedSpy'); + layer.on('styleChanged', styleChangedSpy); + + layer.setStyle(style); + + expect(styleChangedSpy).not.toHaveBeenCalled(); + }); + }); + + describe('when the layer has an engine', function () { + var client; + beforeEach(function () { + client = new carto.Client({ + apiKey: '84fdbd587e4a942510270a48e843b4c1baa11e18', + username: 'cartojs-test' + }); + }); + + describe('and the style has no engine', function () { + it('should set the engine into the style and update the internal style.', function (done) { + client.addLayer(layer) + .then(function () { + return layer.setStyle(newStyle); + }) + .then(function () { + expect(layer.getStyle().$getEngine()).toBe(layer._engine); + done(); + }); + }); + }); + + describe('and the style has an engine', function () { + it('should update the internal style when the engines are equal', function (done) { + newStyle.$setEngine(client._engine); + client.addLayer(layer) + .then(function () { + return layer.setStyle(newStyle); + }) + .then(function () { + expect(layer.getStyle().$getEngine()).toBe(layer._engine); + done(); + }); + }); + + it('should throw an error when the engines are not equal', function (done) { + newStyle.$setEngine('fakeEngine'); + client.addLayer(layer) + .then(function () { + expect(function () { + return layer.setStyle(newStyle); + }).toThrowError('A layer can\'t have a style which belongs to a different client.'); + done(); + }); + }); + }); + + it('should fire a cartoError when the style is invalid', function (done) { + var styleChangedSpy = jasmine.createSpy('styleChangedSpy'); + layer.on('error', styleChangedSpy); + client.addLayer(layer) + .then(function () { + var malformedStyle = new carto.style.CartoCSS('#layer { invalid-rule: foo}'); + return layer.setStyle(malformedStyle); + }) + .catch(function () { + expect(styleChangedSpy).toHaveBeenCalled(); + done(); + }); + }); + + it('should fire a styleChanged event when the layer belongs to a client', function (done) { + var styleChangedSpy = jasmine.createSpy('styleChangedSpy'); + layer.on('styleChanged', styleChangedSpy); + + client.addLayer(layer) + .then(function () { + return layer.setStyle(newStyle); + }) + .then(function () { + expect(styleChangedSpy).toHaveBeenCalled(); + done(); + }); + }); + + it('should set the internal model style', function (done) { + var client = new carto.Client({ + apiKey: '84fdbd587e4a942510270a48e843b4c1baa11e18', + username: 'cartojs-test' + }); + + client.on(carto.events.ERROR, alert); + client.addLayer(layer) + .then(function () { + client.on(carto.events.SUCCESS, function () { + var expected = '#layer { marker-fill: green; }'; + var actual = layer.$getInternalModel().get('cartocss'); + expect(expected).toEqual(actual); + done(); + }); + layer.setStyle(newStyle); + }); + }); + }); + }); + + describe('.$setEngine', function () { + it('probando', function () { + var layer = new carto.layer.Layer(source, style); + var engineMock = { on: jasmine.createSpy('on'), reload: jasmine.createSpy('reload').and.returnValue(Promise.resolve()) }; + var error = { + message: 'an error' + }; + var capturedError; + layer.$setEngine(engineMock); + layer.on(carto.events.ERROR, function (error) { + capturedError = error; + }); + + layer._internalModel.set('error', error); + + expect(capturedError).toBeDefined(); + expect(capturedError.name).toEqual('CartoError'); + expect(capturedError.message).toEqual('an error'); + }); + }); + + describe('.setSource', function () { + var layer; + var newSource; + + beforeEach(function () { + layer = new carto.layer.Layer(source, style); + newSource = new carto.source.SQL('SELECT * FROM ne_10m_populated_places_simple LIMIT 10'); + }); + + it('should throw an error when the source is not a valid parameter', function () { + expect(function () { + layer.setSource('bad-parameter'); + }).toThrowError('The given object is not a valid source. See "carto.source.Base".'); + }); + + describe('when the layer hasn\'t been set an engine', function () { + it('should normally add the source', function () { + layer.setSource(newSource); + + expect(layer.getSource()).toEqual(newSource); + }); + + it('should fire a sourceChanged event', function (done) { + layer.on('sourceChanged', function (l) { + expect(l).toBe(layer); + expect(l.getSource()).toEqual(newSource); + done(); + }); + + layer.setSource(newSource); + }); + }); + + describe('when the layer has been set an engine', function () { + var engineMock; + beforeEach(function () { + engineMock = { on: jasmine.createSpy('on'), reload: jasmine.createSpy('reload').and.returnValue(Promise.resolve()) }; + layer.$setEngine(engineMock); + }); + + describe('and the source has no engine', function () { + it('should normally add the source', function (done) { + layer.setSource(newSource) + .then(function () { + var actualSource = layer.$getInternalModel().get('source'); + var expectedSource = newSource.$getInternalModel(); + expect(actualSource).toBeDefined(); + expect(actualSource).toEqual(expectedSource); + done(); + }); + }); + + it('should fire a sourceChanged event', function (done) { + var sourceChangedSpy = jasmine.createSpy('sourceChangedSpy'); + layer.on('sourceChanged', sourceChangedSpy); + + layer.setSource(newSource) + .then(function () { + expect(sourceChangedSpy).toHaveBeenCalled(); + done(); + }); + }); + }); + + describe('and the source has an engine', function () { + it('should add the source if the engines are the same', function () { + newSource.$setEngine(engineMock); + + layer.setSource(newSource); + + var actualSource = layer.$getInternalModel().get('source'); + var expectedSource = newSource.$getInternalModel(); + expect(actualSource).toEqual(expectedSource); + }); + + it('should throw an error if the engines are different', function () { + // This engine is different from the layer's one. + var engineMock1 = { on: jasmine.createSpy('on'), reload: jasmine.createSpy('reload') }; + newSource.$setEngine(engineMock1); + + expect(function () { + layer.setSource(newSource); + }).toThrowError('A layer can\'t have a source which belongs to a different client.'); + }); + }); + }); + + it('should not fire a sourceChanged event when setting the same source twice', function () { + var sourceChangedSpy = jasmine.createSpy('sourceChangedSpy'); + layer.on('sourceChanged', sourceChangedSpy); + + layer.setSource(source); + + expect(sourceChangedSpy).not.toHaveBeenCalled(); + }); + + it('should fire a cartoError when the source is invalid', function (done) { + var client = new carto.Client({ + apiKey: '84fdbd587e4a942510270a48e843b4c1baa11e18', + username: 'cartojs-test' + }); + var sourceChangedSoy = jasmine.createSpy('sourceChangedSoy'); + layer.on('error', sourceChangedSoy); + client.addLayer(layer) + .then(function () { + var invalidDataset = new carto.source.Dataset('invalid_dataset'); + return layer.setSource(invalidDataset); + }) + .catch(function () { + expect(sourceChangedSoy).toHaveBeenCalled(); + done(); + }); + }); + }); + + describe('.setFeatureClickColumns', function () { + var layer; + var newColums; + + beforeEach(function () { + layer = new carto.layer.Layer(source, style); + newColums = ['a', 'b']; + }); + + it('should throw an error when the columns are not a valid parameter', function () { + expect(function () { + layer.setFeatureClickColumns([1, 2, 3]); + }).toThrowError('The given object is not a valid array of string columns.'); + }); + + describe('when the layer hasn\'t been set an engine', function () { + it('should normally add the columns', function () { + layer.setFeatureClickColumns(newColums); + + expect(layer.getFeatureClickColumns()).toEqual(newColums); + }); + }); + + describe('when the layer has been set an engine', function () { + var engineMock; + + it('should normally add the columns', function (done) { + engineMock = { on: jasmine.createSpy('on'), reload: jasmine.createSpy('reload').and.returnValue(Promise.resolve()) }; + layer.$setEngine(engineMock); + + layer.setFeatureClickColumns(newColums) + .then(function () { + var actualColumns = layer.getFeatureClickColumns(); + var expectedColumns = newColums; + expect(actualColumns).toBeDefined(); + expect(actualColumns).toEqual(expectedColumns); + done(); + }); + }); + + it('should throw an error when the columns are not valid', function (done) { + engineMock = { on: jasmine.createSpy('on'), reload: jasmine.createSpy('reload').and.returnValue(Promise.reject(new Error())) }; + layer.$setEngine(engineMock); + + layer.setFeatureClickColumns(['wrong']) + .catch(function (error) { + expect(error instanceof Error).toBe(true); + done(); + }); + }); + + it('should fire an error event when the columns are not valid', function (done) { + engineMock = { on: jasmine.createSpy('on'), reload: jasmine.createSpy('reload').and.returnValue(Promise.reject(new Error())) }; + layer.$setEngine(engineMock); + + var columnChangedError = jasmine.createSpy('columnChangedError'); + layer.on('error', columnChangedError); + + layer.setFeatureClickColumns(['wrong']) + .catch(function () { + expect(columnChangedError).toHaveBeenCalled(); + done(); + }); + }); + }); + }); + + describe('.setFeatureOverColumns', function () { + var layer; + var newColums; + + beforeEach(function () { + layer = new carto.layer.Layer(source, style); + newColums = ['a', 'b']; + }); + + it('should throw an error when the columns are not a valid parameter', function () { + expect(function () { + layer.setFeatureOverColumns([1, 2, 3]); + }).toThrowError('The given object is not a valid array of string columns.'); + }); + + describe('when the layer hasn\'t been set an engine', function () { + it('should normally add the columns', function () { + layer.setFeatureOverColumns(newColums); + + expect(layer.getFeatureOverColumns()).toEqual(newColums); + }); + }); + + describe('when the layer has been set an engine', function () { + var engineMock; + + it('should normally add the columns', function (done) { + engineMock = { on: jasmine.createSpy('on'), reload: jasmine.createSpy('reload').and.returnValue(Promise.resolve()) }; + layer.$setEngine(engineMock); + + layer.setFeatureOverColumns(newColums) + .then(function () { + var actualColumns = layer.getFeatureOverColumns(); + var expectedColumns = newColums; + expect(actualColumns).toBeDefined(); + expect(actualColumns).toEqual(expectedColumns); + done(); + }); + }); + + it('should throw an error when the columns are not valid', function (done) { + engineMock = { on: jasmine.createSpy('on'), reload: jasmine.createSpy('reload').and.returnValue(Promise.reject(new Error())) }; + layer.$setEngine(engineMock); + + layer.setFeatureOverColumns(['wrong']) + .catch(function (error) { + expect(error instanceof Error).toBe(true); + done(); + }); + }); + + it('should fire an error event when the columns are not valid', function (done) { + engineMock = { on: jasmine.createSpy('on'), reload: jasmine.createSpy('reload').and.returnValue(Promise.reject(new Error())) }; + layer.$setEngine(engineMock); + + var columnChangedError = jasmine.createSpy('columnChangedError'); + layer.on('error', columnChangedError); + + layer.setFeatureOverColumns(['wrong']) + .catch(function () { + expect(columnChangedError).toHaveBeenCalled(); + done(); + }); + }); + }); + }); + + describe('.show', function () { + it('should set the layer visibility to true', function () { + var layer = new carto.layer.Layer(source, style); + expect(layer.isVisible()).toEqual(true); + + layer.hide(); + + expect(layer.isVisible()).toEqual(false); + expect(layer.isHidden()).toEqual(true); + + layer.show(); + + expect(layer.isVisible()).toEqual(true); + expect(layer.isHidden()).toEqual(false); + }); + + it('should trigger a visibilityChanged event', function (done) { + var layer = new carto.layer.Layer(source, style); + expect(layer.isVisible()).toEqual(true); + + layer.hide(); + + layer.on('visibilityChanged', function () { + expect(layer.isVisible()).toEqual(true); + expect(layer.isHidden()).toEqual(false); + done(); + }); + + layer.show(); + }); + }); + + describe('.hide', function () { + it('should set the layer visibility to false', function () { + var layer = new carto.layer.Layer(source, style); + expect(layer.isVisible()).toEqual(true); + + layer.hide(); + + expect(layer.isVisible()).toEqual(false); + expect(layer.isHidden()).toEqual(true); + }); + + it('should trigger a visibilityChanged event', function (done) { + var layer = new carto.layer.Layer(source, style); + expect(layer.isVisible()).toEqual(true); + + layer.on('visibilityChanged', function () { + expect(layer.isVisible()).toEqual(false); + expect(layer.isHidden()).toEqual(true); + done(); + }); + + layer.hide(); + }); + }); + + describe('.toggle', function () { + it('should toggle the layer visibility', function () { + var layer = new carto.layer.Layer(source, style); + expect(layer.isVisible()).toEqual(true); + + layer.toggle(); + + expect(layer.isVisible()).toEqual(false); + expect(layer.isHidden()).toEqual(true); + + layer.toggle(); + + expect(layer.isVisible()).toEqual(true); + expect(layer.isHidden()).toEqual(false); + }); + + it('should trigger a visibilityChanged event', function (done) { + var layer = new carto.layer.Layer(source, style); + expect(layer.isVisible()).toEqual(true); + + layer.on('visibilityChanged', function () { + expect(layer.isVisible()).toEqual(false); + expect(layer.isHidden()).toEqual(true); + done(); + }); + + layer.toggle(); + }); + }); + + describe('.setOrder', function () { + it('should call moveLayer with the passed index', function () { + var clientMock = { moveLayer: jasmine.createSpy('moveLayer') }; + var layer = new carto.layer.Layer(source, style); + layer.$setClient(clientMock); + + layer.setOrder(1); + expect(clientMock.moveLayer).toHaveBeenCalledWith(layer, 1); + }); + }); + + describe('.bringToBack', function () { + it('should call moveLayer with the passed index', function () { + var clientMock = { moveLayer: jasmine.createSpy('moveLayer') }; + var layer = new carto.layer.Layer(source, style); + layer.$setClient(clientMock); + + layer.bringToBack(); + expect(clientMock.moveLayer).toHaveBeenCalledWith(layer, 0); + }); + }); + + describe('.bringToFront', function () { + it('should call moveLayer with the passed index', function () { + var numberOfLayers = 3; + var clientMock = { moveLayer: jasmine.createSpy('moveLayer'), _layers: { size: function () { return numberOfLayers; } } }; + var layer = new carto.layer.Layer(source, style); + layer.$setClient(clientMock); + + layer.bringToFront(); + expect(clientMock.moveLayer).toHaveBeenCalledWith(layer, numberOfLayers - 1); + }); + }); + + describe('.isInteractive', function () { + it('returns true if layer has featureClickColumns', function () { + const layer = new carto.layer.Layer(source, style, { + featureClickColumns: ['cartodb_id'] + }); + + expect(layer.isInteractive()).toBe(true); + }); + + it('returns true if layer has featureOverColumns', function () { + const layer = new carto.layer.Layer(source, style, { + featureOverColumns: ['cartodb_id'] + }); + + expect(layer.isInteractive()).toBe(true); + }); + + it('returns false if layer doesn\'t have getFeatureClickColumns or getFeatureHoverColumns', function () { + const layer = new carto.layer.Layer(source, style); + + expect(layer.isInteractive()).toBe(false); + }); + }); + + xit('should update "internalmodel.cartocss" when the style is updated', function (done) { + var client = new carto.Client({ + apiKey: '84fdbd587e4a942510270a48e843b4c1baa11e18', + username: 'cartojs-test' + }); + var layer = new carto.layer.Layer(source, style); + var newStyle = '#layer { marker-fill: #FABADA; }'; + + client.addLayer(layer) + .then(function () { + return style.setContent(newStyle); + }) + .then(function () { + expect(layer.$getInternalModel().get('cartocss')).toEqual(newStyle); + done(); + }); + }); +}); diff --git a/test/spec/api/v4/layer/metadata/parser.spec.js b/test/spec/api/v4/layer/metadata/parser.spec.js new file mode 100644 index 0000000..782bfb2 --- /dev/null +++ b/test/spec/api/v4/layer/metadata/parser.spec.js @@ -0,0 +1,230 @@ +var metadataParser = require('../../../../../../src/api/v4/layer/metadata/parser'); + +describe('api/v4/layer/metadata/parser', function () { + describe('.getMetadataFromRules', function () { + it('should parse correctly the range rules', function () { + // From: + // marker-width: ramp([scalerank], range(5, 20), quantiles(4)); + // marker-fill-opacity: ramp([pop_max], range(0,1), jenks); + var rules = [ + { + 'selector': '#layer', + 'prop': 'marker-width', + 'column': 'scalerank', + 'mapping': '>', + 'buckets': [ + { + 'filter': { + 'type': 'range', + 'start': 0, + 'end': 6 + }, + 'value': 5 + }, + { + 'filter': { + 'type': 'range', + 'start': 6, + 'end': 7 + }, + 'value': 10 + }, + { + 'filter': { + 'type': 'range', + 'start': 7, + 'end': 7 + }, + 'value': 15 + }, + { + 'filter': { + 'type': 'range', + 'start': 7, + 'end': 10 + }, + 'value': 20 + } + ], + 'stats': { + 'filter_avg': 6.642174269325321 + } + }, + { + 'selector': '#layer', + 'prop': 'marker-fill-opacity', + 'column': 'pop_max', + 'mapping': '>', + 'buckets': [ + { + 'filter': { + 'type': 'range', + 'start': -99, + 'end': 37945 + }, + 'value': 0 + }, + { + 'filter': { + 'type': 'range', + 'start': 37945, + 'end': 138954 + }, + 'value': 0.25 + }, + { + 'filter': { + 'type': 'range', + 'start': 138954, + 'end': 616990 + }, + 'value': 0.5 + }, + { + 'filter': { + 'type': 'range', + 'start': 616990, + 'end': 2544000 + }, + 'value': 0.75 + }, + { + 'filter': { + 'type': 'range', + 'start': 2544000, + 'end': 35676000 + }, + 'value': 1 + } + ], + 'stats': { + 'filter_avg': 322717.4763725758 + } + } + ]; + var metadataList = metadataParser.getMetadataFromRules(rules); + + expect(metadataList).toBeDefined(); + + expect(metadataList[0].getType()).toBe('buckets'); + expect(metadataList[0].getColumn()).toBe('scalerank'); + expect(metadataList[0].getMapping()).toBe('>'); + expect(metadataList[0].getProperty()).toBe('marker-width'); + expect(metadataList[0].getAverage()).toBe(6.642174269325321); + expect(metadataList[0].getMin()).toBe(0); + expect(metadataList[0].getMax()).toBe(10); + expect(metadataList[0].getBuckets()).toEqual([ + { min: 0, max: 6, value: 5 }, + { min: 6, max: 7, value: 10 }, + { min: 7, max: 7, value: 15 }, + { min: 7, max: 10, value: 20 } + ]); + + expect(metadataList[0].getType()).toBe('buckets'); + expect(metadataList[1].getColumn()).toBe('pop_max'); + expect(metadataList[1].getMapping()).toBe('>'); + expect(metadataList[1].getProperty()).toBe('marker-fill-opacity'); + expect(metadataList[1].getAverage()).toBe(322717.4763725758); + expect(metadataList[1].getMin()).toBe(-99); + expect(metadataList[1].getMax()).toBe(35676000); + expect(metadataList[1].getBuckets()).toEqual([ + { min: -99, max: 37945, value: 0 }, + { min: 37945, max: 138954, value: 0.25 }, + { min: 138954, max: 616990, value: 0.5 }, + { min: 616990, max: 2544000, value: 0.75 }, + { min: 2544000, max: 35676000, value: 1 } + ]); + }); + + it('should parse correctly the category rules', function () { + // From: + // marker-fill: ramp([scalerank], (#5F4690, #1D6996, #38A6A5, #666666), (7, 6, 10), '=', category); + // marker-file: ramp([scalerank], (url('https://s3.amazonaws.com/com.cartodb.users-assets.production/maki-icons/rail-light-18.svg'), url('https://s3.amazonaws.com/com.cartodb.users-assets.production/maki-icons/park-18.svg')), (7, 10), '='); + var rules = [ + { + 'selector': '#layer', + 'prop': 'marker-fill', + 'column': 'scalerank', + 'mapping': '=', + 'buckets': [ + { + 'filter': { + 'name': 7, + 'type': 'category' + }, + 'value': '#5F4690' + }, + { + 'filter': { + 'name': 6, + 'type': 'category' + }, + 'value': '#1D6996' + }, + { + 'filter': { + 'name': 10, + 'type': 'category' + }, + 'value': '#38A6A5' + }, + { + 'filter': { + 'type': 'default' + }, + 'value': '#666666' + } + ], + 'stats': {} + }, + { + 'selector': '', + 'prop': 'marker-file', + 'column': 'scalerank', + 'mapping': '=', + 'buckets': [ + { + 'filter': { + 'name': 7, + 'type': 'category' + }, + 'value': 'url(\'https://s3.amazonaws.com/com.cartodb.users-assets.production/maki-icons/rail-light-18.svg\')' + }, + { + 'filter': { + 'name': 10, + 'type': 'category' + }, + 'value': 'url(\'https://s3.amazonaws.com/com.cartodb.users-assets.production/maki-icons/park-18.svg\')' + } + ], + 'stats': {} + } + ]; + var metadataList = metadataParser.getMetadataFromRules(rules); + + expect(metadataList).toBeDefined(); + + expect(metadataList[0].getType()).toBe('categories'); + expect(metadataList[0].getColumn()).toBe('scalerank'); + expect(metadataList[0].getMapping()).toBe('='); + expect(metadataList[0].getProperty()).toBe('marker-fill'); + expect(metadataList[0].getDefaultValue()).toBe('#666666'); + expect(metadataList[0].getCategories()).toEqual([ + { name: 7, value: '#5F4690' }, + { name: 6, value: '#1D6996' }, + { name: 10, value: '#38A6A5' } + ]); + + expect(metadataList[0].getType()).toBe('categories'); + expect(metadataList[1].getColumn()).toBe('scalerank'); + expect(metadataList[1].getMapping()).toBe('='); + expect(metadataList[1].getProperty()).toBe('marker-file'); + expect(metadataList[1].getDefaultValue()).not.toBeDefined(); + expect(metadataList[1].getCategories()).toEqual([ + { name: 7, value: 'url(\'https://s3.amazonaws.com/com.cartodb.users-assets.production/maki-icons/rail-light-18.svg\')' }, + { name: 10, value: 'url(\'https://s3.amazonaws.com/com.cartodb.users-assets.production/maki-icons/park-18.svg\')' } + ]); + }); + }); +}); diff --git a/test/spec/api/v4/layers.spec.js b/test/spec/api/v4/layers.spec.js new file mode 100644 index 0000000..a77fc95 --- /dev/null +++ b/test/spec/api/v4/layers.spec.js @@ -0,0 +1,41 @@ +var Layers = require('../../../../src/api/v4/layers'); + +describe('api/v4/layers', function () { + var layers; + + function createFakeLayer (id) { + return { + getId: function () { + return id; + } + }; + } + + function seedLayers (layers) { + var layerA = createFakeLayer('A'); + var layerB = createFakeLayer('B'); + var layerC = createFakeLayer('C'); + + layers.add(layerA); + layers.add(layerB); + layers.add(layerC); + + return [layerA, layerB, layerC]; + } + + beforeEach(function () { + layers = new Layers(); + }); + + describe('.remove', function () { + it('should remove the layer from the collection', function () { + var createdLayers = seedLayers(layers); + + layers.remove(createdLayers[1]); + + expect(layers.size()).toBe(2); + expect(layers.indexOf(createdLayers[0])).toBe(0); + expect(layers.indexOf(createdLayers[2])).toBe(1); + }); + }); +}); diff --git a/test/spec/api/v4/native/google-maps-map-type.spec.js b/test/spec/api/v4/native/google-maps-map-type.spec.js new file mode 100644 index 0000000..15324b9 --- /dev/null +++ b/test/spec/api/v4/native/google-maps-map-type.spec.js @@ -0,0 +1,141 @@ +/* global google */ +var carto = require('../../../../../src/api/v4'); + +describe('src/api/v4/native/google-maps-map-type', function () { + var client; + var layer; + var mapType; + var map; + + beforeEach(function () { + var element = document.createElement('div'); + element.id = 'map'; + document.body.appendChild(element); + + client = new carto.Client({ + apiKey: '84fdbd587e4a942510270a48e843b4c1baa11e18', + username: 'cartojs-test' + }); + map = new google.maps.Map(element); + mapType = client.getGoogleMapsMapType(map); + }); + + afterEach(function () { + document.getElementById('map').remove(); + }); + + describe('layer events', function () { + var spy; + var internalEventMock; + + beforeEach(function () { + spy = jasmine.createSpy('spy'); + + map.overlayMapTypes.push(mapType); + + var source = new carto.source.SQL('foo'); + var style = new carto.style.CartoCSS('bar'); + layer = new carto.layer.Layer(source, style); + + client.addLayer(layer, { reload: false }); + + internalEventMock = { + layer: { + id: layer.getId() + }, + latlng: [ 10, 20 ], + feature: { name: 'foo' } + }; + }); + + it('should trigger carto.layer.events.FEATURE_CLICKED event', function () { + layer.on(carto.layer.events.FEATURE_CLICKED, spy); + + var expectedExternalEvent = { + data: { name: 'foo' }, + latLng: { lat: 10, lng: 20 } + }; + + mapType._internalView.trigger('featureClick', internalEventMock); + + expect(spy).toHaveBeenCalledWith(expectedExternalEvent); + }); + + it('should trigger carto.layer.events.FEATURE_OVER event', function () { + layer.on(carto.layer.events.FEATURE_OVER, spy); + + var expectedExternalEvent = { + data: { name: 'foo' }, + latLng: { lat: 10, lng: 20 } + }; + + mapType._internalView.trigger('featureOver', internalEventMock); + + expect(spy).toHaveBeenCalledWith(expectedExternalEvent); + }); + + it('should trigger carto.layer.events.FEATURE_OUT featureOut events', function () { + layer.on(carto.layer.events.FEATURE_OUT, spy); + + var expectedExternalEvent = { + data: { name: 'foo' }, + latLng: { lat: 10, lng: 20 } + }; + mapType._internalView.trigger('featureOut', internalEventMock); + + expect(spy).toHaveBeenCalledWith(expectedExternalEvent); + }); + + describe('mouse pointer', function () { + describe('when mousing over a feature', function () { + it("should NOT set the mouse cursor to 'pointer' if layer doesn't have featureOverColumns or featureClickColumns", function () { + expect(map.get('draggableCursor')).not.toBeDefined(); + + mapType._internalView.trigger('featureOver', internalEventMock); + + expect(map.get('draggableCursor')).not.toBeDefined(); + }); + + it("should set the mouse cursor to 'pointer' if layer has featureOverColumns", function () { + layer._featureOverColumns = [ 'foo' ]; + + expect(map.get('draggableCursor')).not.toBeDefined(); + + mapType._internalView.trigger('featureOver', internalEventMock); + + expect(map.get('draggableCursor')).toEqual('pointer'); + }); + + it("should set the mouse cursor to 'pointer' if layer has featureClickColumns", function () { + layer._featureClickColumns = [ 'foo' ]; + + expect(map.get('draggableCursor')).not.toBeDefined(); + + mapType._internalView.trigger('featureOver', internalEventMock); + + expect(map.get('draggableCursor')).toEqual('pointer'); + }); + + it("should set the mouse cursor to 'pointer' if layer has overed features after a featureOut", function () { + mapType._hoveredLayers = ['L100']; + + expect(map.get('draggableCursor')).not.toBeDefined(); + + mapType._internalView.trigger('featureOut', internalEventMock); + + expect(map.get('draggableCursor')).toEqual('pointer'); + }); + }); + + describe('when mousing over NO features', function () { + it("should set the mouse cursor to 'auto'", function () { + expect(map.get('draggableCursor')).not.toBeDefined(); + + mapType._internalView.trigger('featureOut', internalEventMock); + + expect(map.get('draggableCursor')).toEqual('auto'); + }); + }); + }); + }); +}); diff --git a/test/spec/api/v4/native/leaflet-layer.spec.js b/test/spec/api/v4/native/leaflet-layer.spec.js new file mode 100644 index 0000000..94c7ddc --- /dev/null +++ b/test/spec/api/v4/native/leaflet-layer.spec.js @@ -0,0 +1,198 @@ +/* global L */ +var carto = require('../../../../../src/api/v4'); + +describe('src/api/v4/native/leaflet-layer', function () { + var client; + var layer; + var leafletLayer; + var map; + + beforeEach(function () { + var element = document.createElement('div'); + element.id = 'map'; + document.body.appendChild(element); + + client = new carto.Client({ + apiKey: '84fdbd587e4a942510270a48e843b4c1baa11e18', + username: 'cartojs-test' + }); + map = L.map('map').setView([42.431234, -8.643616], 5); + }); + + afterEach(function () { + document.getElementById('map').remove(); + }); + + it('allows custom options', function () { + leafletLayer = client.getLeafletLayer({ maxZoom: 10 }); + + expect(leafletLayer.options.maxZoom).toBe(10); + }); + + describe('addTo', function () { + beforeEach(function () { + leafletLayer = client.getLeafletLayer(); + }); + + it('should add a leaflet layer to the map', function () { + expect(countLeafletLayers(map)).toEqual(0); + + leafletLayer.addTo(map); + + expect(countLeafletLayers(map)).toEqual(1); + }); + }); + + describe('removeFrom', function () { + beforeEach(function () { + leafletLayer = client.getLeafletLayer(); + }); + + it('should remove the leaflet layer from the map', function () { + expect(countLeafletLayers(map)).toEqual(0); + + leafletLayer.addTo(map); + + expect(countLeafletLayers(map)).toEqual(1); + + leafletLayer.removeFrom(map); + + expect(countLeafletLayers(map)).toEqual(0); + }); + }); + + describe('layer events', function () { + var spy; + var internalEventMock; + + beforeEach(function () { + spy = jasmine.createSpy('spy'); + + leafletLayer = client.getLeafletLayer(); + leafletLayer.addTo(map); + + var source = new carto.source.SQL('foo'); + var style = new carto.style.CartoCSS('bar'); + layer = new carto.layer.Layer(source, style); + + client.addLayer(layer); + + internalEventMock = { + layer: { + id: layer.getId() + }, + latlng: [ 10, 20 ], + feature: { name: 'foo' } + }; + }); + + it('should trigger carto.layer.events.FEATURE_CLICKED event', function () { + layer.on(carto.layer.events.FEATURE_CLICKED, spy); + + var expectedExternalEvent = { + data: { name: 'foo' }, + latLng: { lat: 10, lng: 20 } + }; + + leafletLayer._internalView.trigger('featureClick', internalEventMock); + + expect(spy).toHaveBeenCalledWith(expectedExternalEvent); + }); + + it('should trigger carto.layer.events.FEATURE_OVER event', function () { + layer.on(carto.layer.events.FEATURE_OVER, spy); + + var expectedExternalEvent = { + data: { name: 'foo' }, + latLng: { lat: 10, lng: 20 } + }; + + leafletLayer._internalView.trigger('featureOver', internalEventMock); + + expect(spy).toHaveBeenCalledWith(expectedExternalEvent); + }); + + it('should trigger carto.layer.events.FEATURE_OUT featureOut events', function () { + layer.on(carto.layer.events.FEATURE_OUT, spy); + + var expectedExternalEvent = { + data: { name: 'foo' }, + latLng: { lat: 10, lng: 20 } + }; + leafletLayer._internalView.trigger('featureOut', internalEventMock); + + expect(spy).toHaveBeenCalledWith(expectedExternalEvent); + }); + + it('should trigger carto.layer.events.TILE_ERROR events', function () { + layer.on(carto.layer.events.TILE_ERROR, spy); + layer._featureClickColumns = [ 'foo' ]; + var error = { + message: 'an error' + }; + + leafletLayer._internalView.trigger('featureError', error); + + expect(spy).toHaveBeenCalledWith(jasmine.objectContaining({ + name: 'CartoError', + message: 'an error' + })); + }); + + describe('mouse pointer', function () { + describe('when mousing over a feature', function () { + it("should NOT set the mouse cursor to 'pointer' if layer doesn't have featureOverColumns or featureClickColumns", function () { + expect(map.getContainer().style.cursor).toEqual(''); + + leafletLayer._internalView.trigger('featureOver', internalEventMock); + + expect(map.getContainer().style.cursor).toEqual(''); + }); + + it("should set the mouse cursor to 'pointer' if layer has featureOverColumns", function () { + layer._featureOverColumns = [ 'foo' ]; + + expect(map.getContainer().style.cursor).toEqual(''); + + leafletLayer._internalView.trigger('featureOver', internalEventMock); + + expect(map.getContainer().style.cursor).toEqual('pointer'); + }); + + it("should set the mouse cursor to 'pointer' if layer has featureClickColumns", function () { + layer._featureClickColumns = [ 'foo' ]; + + expect(map.getContainer().style.cursor).toEqual(''); + + leafletLayer._internalView.trigger('featureOver', internalEventMock); + + expect(map.getContainer().style.cursor).toEqual('pointer'); + }); + + it("should set the mouse cursor to 'pointer' if layer has overed features after a featureOut", function () { + leafletLayer._hoveredLayers = ['L100']; + + expect(map.getContainer().style.cursor).toEqual(''); + + leafletLayer._internalView.trigger('featureOut', internalEventMock); + + expect(map.getContainer().style.cursor).toEqual('pointer'); + }); + }); + + describe('when mousing over NO features', function () { + it("should set the mouse cursor to 'auto'", function () { + expect(map.getContainer().style.cursor).toEqual(''); + + leafletLayer._internalView.trigger('featureOut', internalEventMock); + + expect(map.getContainer().style.cursor).toEqual('auto'); + }); + }); + }); + }); + + function countLeafletLayers (map) { + return Object.keys(map._layers).length; + } +}); diff --git a/test/spec/api/v4/source/dataset.spec.js b/test/spec/api/v4/source/dataset.spec.js new file mode 100644 index 0000000..0457671 --- /dev/null +++ b/test/spec/api/v4/source/dataset.spec.js @@ -0,0 +1,268 @@ +const Base = require('../../../../../src/api/v4/source/base'); +const carto = require('../../../../../src/api/v4'); + +describe('api/v4/source/dataset', function () { + var originalTimeout; + + beforeEach(function () { + originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; + jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; + }); + + afterEach(function () { + jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; + }); + + describe('constructor', function () { + it('should return a new Dataset object', function () { + var populatedPlacesDataset = new carto.source.Dataset('ne_10m_populated_places_simple'); + expect(populatedPlacesDataset).toBeDefined(); + }); + + it('should autogenerate an id when no ID is given', function () { + var populatedPlacesDataset = new carto.source.Dataset('ne_10m_populated_places_simple'); + expect(populatedPlacesDataset.getId()).toMatch(/S\d+/); + }); + + it('should throw an error if tableName is not provided', function () { + expect(function () { + new carto.source.Dataset(); // eslint-disable-line + }).toThrowError('Table name is required.'); + }); + + it('should throw an error if tableName is empty', function () { + expect(function () { + new carto.source.Dataset(''); // eslint-disable-line + }).toThrowError('Table name must be not empty.'); + }); + + it('should throw an error if tableName is not a valid string', function () { + expect(function () { + new carto.source.Dataset(3333); // eslint-disable-line + }).toThrowError('Table name must be a string.'); + }); + }); + + describe('.setTableName', function () { + let populatedPlacesDataset; + + beforeEach(function () { + populatedPlacesDataset = new carto.source.Dataset('ne_10m_populated_places_simple'); + }); + + it('should set the dataset', function () { + populatedPlacesDataset.setTableName('airbnb_listings'); + expect(populatedPlacesDataset.getTableName()).toEqual('airbnb_listings'); + }); + + it('should throw an error if query is empty', function () { + expect(function () { + populatedPlacesDataset.setTableName(undefined); + }).toThrowError('Table name is required.'); + }); + + it('should throw an error if query is not a valid string', function () { + expect(function () { + populatedPlacesDataset.setTableName(333); + }).toThrowError('Table name must be a string.'); + }); + + it('should throw an error if query is empty', function () { + expect(function () { + populatedPlacesDataset.setTableName(''); + }).toThrowError('Table name must be not empty.'); + }); + + it('should trigger an tableNameChanged event when there is no internal model', function (done) { + const expectedTable = 'airbnb_listings'; + + populatedPlacesDataset.on('tableNameChanged', function (newQuery) { + expect(newQuery).toEqual(expectedTable); + done(); + }); + + populatedPlacesDataset.setTableName(expectedTable); + }); + + it('should trigger an tableNameChanged event when there is an internal model', function (done) { + const client = new carto.Client({ + apiKey: '84fdbd587e4a942510270a48e843b4c1baa11e18', + username: 'cartojs-test' + }); + const style = new carto.style.CartoCSS('#layer { marker-fill: red; }'); + const layer = new carto.layer.Layer(populatedPlacesDataset, style); + const tableNameChangedSpy = jasmine.createSpy('tableNameChangedSpy'); + const newTableName = 'airbnb_listings'; + + populatedPlacesDataset.on('tableNameChanged', tableNameChangedSpy); + + client.addLayer(layer) + .then(function () { + return populatedPlacesDataset.setTableName(newTableName); + }) + .then(function () { + expect(tableNameChangedSpy).toHaveBeenCalledWith(newTableName); + done(); + }); + }); + + it('should return a resolved promise when there is no internal model', function (done) { + const newTableName = 'airbnb_listings'; + populatedPlacesDataset.setTableName(newTableName) + .then(function () { + expect(populatedPlacesDataset.getTableName()).toEqual(newTableName); + done(); + }); + }); + + it('should return a resolved promise when there is an internal model', function (done) { + const client = new carto.Client({ + apiKey: '84fdbd587e4a942510270a48e843b4c1baa11e18', + username: 'cartojs-test' + }); + const style = new carto.style.CartoCSS('#layer { marker-fill: red; }'); + const layer = new carto.layer.Layer(populatedPlacesDataset, style); + const newTableName = 'airbnb_listings'; + + client.addLayer(layer) + .then(function () { + return populatedPlacesDataset.setTableName(newTableName); + }) + .then(function () { + expect(populatedPlacesDataset.getTableName()).toEqual(newTableName); + done(); + }); + }); + + it('should return a rejected promise with a CartoError when there is an internal model (and a reload error)', function (done) { + const client = new carto.Client({ + apiKey: '84fdbd587e4a942510270a48e843b4c1baa11e18', + username: 'cartojs-test' + }); + const style = new carto.style.CartoCSS('#layer { marker-fill: red; }'); + const layer = new carto.layer.Layer(populatedPlacesDataset, style); + const newTableName = 'invalid_dataset'; + + client.addLayer(layer) + .then(function () { + return populatedPlacesDataset.setTableName(newTableName); + }) + .catch(function (cartoError) { + expect(cartoError.message).toMatch(/Invalid dataset name used. Dataset "invalid_dataset" does not exist./); + done(); + }); + }); + }); + + describe('.getTableName', function () { + it('should return the table name', function () { + var dataset = new carto.source.Dataset('ne_10m_populated_places_simple'); + expect(dataset.getTableName()).toBe('ne_10m_populated_places_simple'); + }); + }); + + describe('$setEngine', function () { + it('should create an internal model with the dataset and the engine', function () { + var populatedPlacesDataset = new carto.source.Dataset('ne_10m_populated_places_simple'); + + populatedPlacesDataset.$setEngine('fakeEngine'); + + var internalModel = populatedPlacesDataset.$getInternalModel(); + expect(internalModel.get('id')).toEqual(populatedPlacesDataset.getId()); + expect(internalModel.get('query')).toEqual('SELECT * from ne_10m_populated_places_simple'); + expect(internalModel._engine).toEqual('fakeEngine'); + }); + }); + + describe('.getQueryToApply', function () { + let populatedPlacesDataset; + + beforeEach(function () { + populatedPlacesDataset = new carto.source.Dataset('ne_10m_populated_places_simple'); + spyOn(populatedPlacesDataset, '_updateInternalModelQuery'); + }); + + it('should return original query if applied filters returns no SQL', function () { + expect(populatedPlacesDataset._getQueryToApply()).toBe('SELECT * from ne_10m_populated_places_simple'); + }); + + it('should return wrapped query if filters are applied', function () { + populatedPlacesDataset.addFilter(new carto.filter.Category('fake_column', { in: ['category'] })); + + expect(populatedPlacesDataset._getQueryToApply()).toBe("SELECT * FROM (SELECT * from ne_10m_populated_places_simple) as datasetQuery WHERE fake_column IN ('category')"); + }); + }); + + describe('.addFilter', function () { + let populatedPlacesDataset; + + beforeEach(function () { + spyOn(Base.prototype, 'addFilter'); + + populatedPlacesDataset = new carto.source.Dataset('ne_10m_populated_places_simple'); + spyOn(populatedPlacesDataset, '_updateInternalModelQuery'); + }); + + it('should call original addFilter and _updateInternalModelQuery', function () { + populatedPlacesDataset.addFilter(new carto.filter.Category('fake_column', { in: ['category'] })); + + expect(Base.prototype.addFilter).toHaveBeenCalled(); + expect(populatedPlacesDataset._updateInternalModelQuery).toHaveBeenCalledWith(populatedPlacesDataset._getQueryToApply()); + }); + }); + + describe('.removeFilter', function () { + let populatedPlacesDataset, filter; + + beforeEach(function () { + spyOn(Base.prototype, 'addFilter'); + + populatedPlacesDataset = new carto.source.Dataset('ne_10m_populated_places_simple'); + spyOn(populatedPlacesDataset, '_updateInternalModelQuery'); + + filter = populatedPlacesDataset.addFilter(new carto.filter.Category('fake_column', { in: ['category'] })); + populatedPlacesDataset.addFilter(filter); + }); + + it('should call original removeFilter and _updateInternalModelQuery', function () { + populatedPlacesDataset.removeFilter(filter); + + expect(Base.prototype.addFilter).toHaveBeenCalled(); + expect(populatedPlacesDataset._updateInternalModelQuery).toHaveBeenCalledWith(populatedPlacesDataset._getQueryToApply()); + }); + }); + + describe('.getFilters', function () { + let populatedPlacesDataset, filter; + + beforeEach(function () { + filter = new carto.filter.Category('fake_column', { in: ['category'] }); + + populatedPlacesDataset = new carto.source.Dataset('ne_10m_populated_places_simple'); + populatedPlacesDataset.addFilter(filter); + }); + + it('should return added filters', function () { + expect(populatedPlacesDataset.getFilters()).toEqual([filter]); + }); + }); + + describe('errors', function () { + it('should trigger an error when invalid', function (done) { + var client = new carto.Client({ + apiKey: '84fdbd587e4a942510270a48e843b4c1baa11e18', + username: 'cartojs-test' + }); + var invalidSource = new carto.source.Dataset('invalid_dataset'); + var cartoCSS = new carto.style.CartoCSS('#layer { marker-fill: red; }'); + + invalidSource.on('error', function (cartoError) { + expect(cartoError.message).toMatch(/Invalid dataset name used. Dataset "invalid_dataset" does not exist./); + done(); + }); + var layer = new carto.layer.Layer(invalidSource, cartoCSS); + + client.addLayer(layer).catch(function () { }); // Prevent console "uncaught error" warning. + }); + }); +}); diff --git a/test/spec/api/v4/source/sql.spec.js b/test/spec/api/v4/source/sql.spec.js new file mode 100644 index 0000000..92af2a0 --- /dev/null +++ b/test/spec/api/v4/source/sql.spec.js @@ -0,0 +1,257 @@ +const Base = require('../../../../../src/api/v4/source/base'); +const carto = require('../../../../../src/api/v4'); + +describe('api/v4/source/sql', function () { + var sqlQuery; + + beforeEach(function () { + sqlQuery = new carto.source.SQL('SELECT * FROM ne_10m_populated_places_simple WHERE adm0name = \'Spain\''); + }); + + describe('constructor', function () { + it('should return a new Dataset object', function () { + expect(sqlQuery).toBeDefined(); + }); + + it('should autogenerate an id', function () { + expect(sqlQuery.getId()).toMatch(/S\d+/); + }); + + it('should throw an error if query is not provided', function () { + expect(function () { + new carto.source.SQL(); // eslint-disable-line + }).toThrowError('SQL Source must have a SQL query.'); + }); + + it('should throw an error if query is empty', function () { + expect(function () { + new carto.source.SQL(''); // eslint-disable-line + }).toThrowError('SQL Source must have a SQL query.'); + }); + + it('should throw an error if query is not a valid string', function () { + expect(function () { + new carto.source.SQL(3333); // eslint-disable-line + }).toThrowError('SQL Query must be a string.'); + }); + }); + + describe('.setQuery', function () { + it('should set the query', function () { + sqlQuery.setQuery('SELECT foo FROM bar'); + expect(sqlQuery.getQuery()).toEqual('SELECT foo FROM bar'); + }); + + it('should throw an error if query is empty', function () { + expect(function () { + sqlQuery.setQuery(''); + }).toThrowError('SQL Source must have a SQL query.'); + }); + + it('should throw an error if query is not a valid string', function () { + expect(function () { + sqlQuery.setQuery(333); + }).toThrowError('SQL Query must be a string.'); + }); + + it('should trigger an queryChanged event when there is no internal model', function (done) { + var expectedQuery = 'SELECT * FROM ne_10m_populated_places_simple LIMIT 10'; + sqlQuery.on('queryChanged', function (newQuery) { + expect(newQuery).toEqual(expectedQuery); + done(); + }); + sqlQuery.setQuery(expectedQuery); + }); + + it('should trigger an queryChanged event when there is an internal model', function (done) { + var client = new carto.Client({ + apiKey: '84fdbd587e4a942510270a48e843b4c1baa11e18', + username: 'cartojs-test' + }); + var style = new carto.style.CartoCSS('#layer { marker-fill: red; }'); + var layer = new carto.layer.Layer(sqlQuery, style); + var queryChangedSpy = jasmine.createSpy('queryChangedSpy'); + var newQuery = 'SELECT * FROM ne_10m_populated_places_simple LIMIT 10'; + + sqlQuery.on('queryChanged', queryChangedSpy); + + client.addLayer(layer) + .then(function () { + return sqlQuery.setQuery(newQuery); + }) + .then(function () { + expect(queryChangedSpy).toHaveBeenCalledWith(newQuery); + done(); + }); + }); + + it('should return a resolved promise when there is no internal model', function (done) { + var newQuery = 'SELECT * FROM ne_10m_populated_places_simple'; + sqlQuery.setQuery(newQuery) + .then(function () { + expect(sqlQuery.getQuery()).toEqual(newQuery); + done(); + }); + }); + + it('should return a resolved promise when there is an internal model', function (done) { + var client = new carto.Client({ + apiKey: '84fdbd587e4a942510270a48e843b4c1baa11e18', + username: 'cartojs-test' + }); + var style = new carto.style.CartoCSS('#layer { marker-fill: red; }'); + var layer = new carto.layer.Layer(sqlQuery, style); + var newQuery = 'SELECT * FROM ne_10m_populated_places_simple LIMIT 10'; + client.addLayer(layer) + .then(function () { + return sqlQuery.setQuery(newQuery); + }) + .then(function () { + expect(sqlQuery.getQuery()).toEqual(newQuery); + done(); + }); + }); + + it('should return a rejected promise with a CartoError when there is an internal model (and a reload error)', function (done) { + var client = new carto.Client({ + apiKey: '84fdbd587e4a942510270a48e843b4c1baa11e18', + username: 'cartojs-test' + }); + var style = new carto.style.CartoCSS('#layer { marker-fill: red; }'); + var layer = new carto.layer.Layer(sqlQuery, style); + var newQuery = 'SELECT * FROM invalid_dataset'; + client.addLayer(layer) + .then(function () { + return sqlQuery.setQuery(newQuery); + }) + .catch(function (cartoError) { + expect(cartoError.message).toMatch(/Invalid dataset name used. Dataset "invalid_dataset" does not exist./); + done(); + }); + }); + }); + + describe('$setEngine', function () { + it('should create an internal model with the dataset attrs and the engine', function () { + sqlQuery.$setEngine('fakeEngine'); + + var internalModel = sqlQuery.$getInternalModel(); + expect(internalModel.get('id')).toEqual(sqlQuery.getId()); + expect(internalModel.get('query')).toEqual('SELECT * FROM ne_10m_populated_places_simple WHERE adm0name = \'Spain\''); + expect(internalModel._engine).toEqual('fakeEngine'); + }); + }); + + describe('.getQueryToApply', function () { + let populatedPlacesSQL; + + beforeEach(function () { + populatedPlacesSQL = new carto.source.SQL('SELECT * FROM ne_10m_populated_places_simple'); + spyOn(populatedPlacesSQL, '_updateInternalModelQuery'); + }); + + it('should return original query if applied filters returns no SQL', function () { + expect(populatedPlacesSQL._getQueryToApply()).toBe('SELECT * FROM ne_10m_populated_places_simple'); + }); + + it('should return wrapped query if filters are applied', function () { + populatedPlacesSQL.addFilter(new carto.filter.Category('fake_column', { in: ['category'] })); + + expect(populatedPlacesSQL._getQueryToApply()).toBe("SELECT * FROM (SELECT * FROM ne_10m_populated_places_simple) as originalQuery WHERE fake_column IN ('category')"); + }); + }); + + describe('.addFilter', function () { + let populatedPlacesSQL; + + beforeEach(function () { + spyOn(Base.prototype, 'addFilter'); + + populatedPlacesSQL = new carto.source.SQL('SELECT * FROM ne_10m_populated_places_simple'); + spyOn(populatedPlacesSQL, '_updateInternalModelQuery'); + }); + + it('should call original addFilter and _updateInternalModelQuery', function () { + populatedPlacesSQL.addFilter(new carto.filter.Category('fake_column', { in: ['category'] })); + + expect(Base.prototype.addFilter).toHaveBeenCalled(); + expect(populatedPlacesSQL._updateInternalModelQuery).toHaveBeenCalledWith(populatedPlacesSQL._getQueryToApply()); + }); + }); + + describe('.removeFilter', function () { + let populatedPlacesDataset, filter; + + beforeEach(function () { + spyOn(Base.prototype, 'addFilter'); + + populatedPlacesDataset = new carto.source.Dataset('ne_10m_populated_places_simple'); + spyOn(populatedPlacesDataset, '_updateInternalModelQuery'); + + filter = populatedPlacesDataset.addFilter(new carto.filter.Category('fake_column', { in: ['category'] })); + populatedPlacesDataset.addFilter(filter); + }); + + it('should call original removeFilter and _updateInternalModelQuery', function () { + populatedPlacesDataset.removeFilter(filter); + + expect(Base.prototype.addFilter).toHaveBeenCalled(); + expect(populatedPlacesDataset._updateInternalModelQuery).toHaveBeenCalledWith(populatedPlacesDataset._getQueryToApply()); + }); + }); + + describe('.getFilters', function () { + let populatedPlacesDataset, filter; + + beforeEach(function () { + filter = new carto.filter.Category('fake_column', { in: ['category'] }); + + populatedPlacesDataset = new carto.source.Dataset('ne_10m_populated_places_simple'); + populatedPlacesDataset.addFilter(filter); + }); + + it('should return added filters', function () { + expect(populatedPlacesDataset.getFilters()).toEqual([filter]); + }); + }); + + describe('errors', function () { + it('should trigger an error when invalid', function (done) { + var client = new carto.Client({ + apiKey: '84fdbd587e4a942510270a48e843b4c1baa11e18', + username: 'cartojs-test' + }); + // The following sql has the invalid operator: === + var invalidSource = new carto.source.SQL('SELECT * FROM ne_10m_populated_places_simple WHERE adm0name === \'Spain\''); + var cartoCss = new carto.style.CartoCSS('#layer { marker-fill: red; }'); + + invalidSource.on('error', function (cartoError) { + expect(cartoError.message).toMatch(/operator does not exist/); + done(); + }); + var layer = new carto.layer.Layer(invalidSource, cartoCss); + + client.addLayer(layer).catch(function () { }); // Prevent console "uncaught error" warning. + }); + + it('should trigger a CartoError when there is an error in the internal model', function () { + var client = new carto.Client({ + apiKey: '84fdbd587e4a942510270a48e843b4c1baa11e18', + username: 'cartojs-test' + }); + var source = new carto.source.SQL('SELECT * FROM ne_10m_populated_places_simple'); + var cartoCss = new carto.style.CartoCSS('#layer { marker-fill: red; }'); + var layer = new carto.layer.Layer(source, cartoCss); + var spy = jasmine.createSpy('spy'); + source.on(carto.events.ERROR, spy); + client.addLayer(layer); + + source._internalModel.set('error', 'an error'); + + expect(spy).toHaveBeenCalledWith(jasmine.objectContaining({ + name: 'CartoError', + originalError: 'an error' + })); + }); + }); +}); diff --git a/test/spec/api/v4/style/cartocss.spec.js b/test/spec/api/v4/style/cartocss.spec.js new file mode 100644 index 0000000..43d0433 --- /dev/null +++ b/test/spec/api/v4/style/cartocss.spec.js @@ -0,0 +1,161 @@ +var carto = require('../../../../../src/api/v4'); + +describe('api/v4/style/cartocss', function () { + var cartoCSS; + + beforeEach(function () { + cartoCSS = new carto.style.CartoCSS('#layer { marker-width:10; }'); + }); + + describe('constructor', function () { + it('should return an object', function () { + expect(cartoCSS).toBeDefined(); + }); + + it('should throw an error if cartoCSS is not provided', function () { + expect(function () { + new carto.style.CartoCSS(); // eslint-disable-line + }).toThrowError('CartoCSS is required.'); + }); + + it('should throw an error if cartoCSS is empty', function () { + expect(function () { + new carto.style.CartoCSS(''); // eslint-disable-line + }).toThrowError('CartoCSS is required.'); + }); + + it('should throw an error if cartoCSS is not a valid string', function () { + expect(function () { + new carto.style.CartoCSS(true); // eslint-disable-line + }).toThrowError('CartoCSS must be a string.'); + }); + }); + + describe('errors', function () { + it('should trigger a CartoError when the style is not valid', function (done) { + var client = new carto.Client({ + apiKey: '84fdbd587e4a942510270a48e843b4c1baa11e18', + username: 'cartojs-test' + }); + var source = new carto.source.Dataset('ne_10m_populated_places_simple'); + var invalidCartoCSS = new carto.style.CartoCSS('#layer { invalid-property: 10; }'); + + invalidCartoCSS.on('error', function (cartoError) { + expect(cartoError.message).toMatch(/Unrecognized rule "invalid-property"/); + done(); + }); + var layer = new carto.layer.Layer(source, invalidCartoCSS); + client.addLayer(layer) + .catch(function () { }); // Prevent console "uncaught error" warning. + }); + + it('should NOT trigger a CartoError when the style is valid but there are some other errors', function (done) { + var client = new carto.Client({ + apiKey: '84fdbd587e4a942510270a48e843b4c1baa11e18', + username: 'cartojs-test' + }); + var source = new carto.source.Dataset('invalid_dataset'); + var invalidCartoCSS = new carto.style.CartoCSS('#layer { marker-with: 10; }'); + var errorCallbackSpy = jasmine.createSpy('errorCallbackSpy').and.callThrough(); + invalidCartoCSS.on('error', errorCallbackSpy); + var layer = new carto.layer.Layer(source, invalidCartoCSS); + + client.addLayer(layer) + .catch(function () { + expect(errorCallbackSpy).not.toHaveBeenCalled(); + done(); + }); + }); + }); + + describe('.getContent', function () { + it('should return the internal style', function () { + var expected = '#layer { marker-width:10; }'; + var actual = cartoCSS.getContent(); + + expect(actual).toEqual(expected); + }); + }); + + describe('.setContent', function () { + var layer; + var source; + var newContent; + + beforeEach(function () { + source = new carto.source.Dataset('ne_10m_populated_places_simple'); + layer = new carto.layer.Layer(source, cartoCSS); + newContent = '#layer { marker-fill: #FABADA }'; + }); + + describe('when no engine is attached', function () { + it('should update the internal style', function (done) { + cartoCSS.setContent(newContent) + .then(function () { + expect(cartoCSS.getContent()).toEqual(newContent); + done(); + }); + }); + + it('should trigger a contentChanged event', function (done) { + var contentChangedSpy = jasmine.createSpy('contentChangedSpy'); + cartoCSS.on('contentChanged', contentChangedSpy); + cartoCSS.setContent(newContent) + .then(function () { + expect(cartoCSS.getContent()).toEqual(newContent); + expect(contentChangedSpy).toHaveBeenCalled(); + done(); + }); + }); + }); + + describe('when an engine is attached', function () { + var client; + + beforeAll(function () { + client = new carto.Client({ + apiKey: '84fdbd587e4a942510270a48e843b4c1baa11e18', + username: 'cartojs-test' + }); + }); + + it('should update the internal style', function (done) { + client.addLayer(layer) + .then(function () { + return cartoCSS.setContent(newContent); + }) + .then(function () { + expect(cartoCSS.getContent()).toEqual(newContent); + done(); + }); + }); + + it('should trigger a contentChanged event?', function (done) { + var contentChangedSpy = jasmine.createSpy('contentChangedSpy'); + cartoCSS.on('contentChanged', contentChangedSpy); + + client.addLayer(layer) + .then(function () { + return cartoCSS.setContent(newContent); + }) + .then(function () { + expect(contentChangedSpy).toHaveBeenCalled(); + done(); + }) + .catch(console.warn); + }); + + it('should reject the promise with a CartoError when the reload fails', function (done) { + var malformedStyle = '#layer { invalid-property: foo }'; + client.addLayer(layer) + .then(function () { + return cartoCSS.setContent(malformedStyle); + }) + .catch(function (cartoError) { + expect(cartoError.message).toMatch(/Unrecognized rule "invalid-property"/); + done(); + }); + }); + }); + }); +}); diff --git a/test/spec/api/vizjson.spec.js b/test/spec/api/vizjson.spec.js new file mode 100644 index 0000000..41f052a --- /dev/null +++ b/test/spec/api/vizjson.spec.js @@ -0,0 +1,352 @@ +var C = require('../../../src/constants'); +var VizJSON = require('../../../src/api/vizjson'); + +describe('src/vis/vizjson', function () { + it('should expose the vizjson attributes', function () { + var vizjson = new VizJSON({ + key1: 'value1', + key2: 'value2' + }); + + expect(vizjson.key1).toEqual('value1'); + expect(vizjson.key2).toEqual('value2'); + }); + + it('should have an attribution overlay by default', function () { + var vizjson = new VizJSON({}); + + expect(vizjson.getOverlayByType(C.OVERLAY_TYPES.ATTRIBUTION)).toEqual({ + type: C.OVERLAY_TYPES.ATTRIBUTION + }); + }); + + describe('.isNamedMap', function () { + it("should return false if datasource doesn't have a template_name", function () { + var vizjson = new VizJSON({ + datasource: { } + }); + + expect(vizjson.isNamedMap()).toBeFalsy(); + }); + + it('should return true if datasource has a template_name', function () { + var vizjson = new VizJSON({ + datasource: { + template_name: 'tpl0123456789' + } + }); + + expect(vizjson.isNamedMap()).toBeTruthy(); + }); + }); + + describe('.hasZoomOverlay', function () { + it("should return true if there's a zoom overlay", function () { + var vizjson = new VizJSON({ + overlays: [{ + type: C.OVERLAY_TYPES.ZOOM + }] + }); + + expect(vizjson.hasZoomOverlay()).toBeTruthy(); + }); + }); + + describe('.hasOverlay', function () { + it("should return true if there's an overlay with the given type", function () { + var vizjson = new VizJSON({ + overlays: [{ + type: 'something' + }] + }); + + expect(vizjson.hasOverlay('something')).toBeTruthy(); + }); + + it("should return false if there isn't an overlay with the given type", function () { + var vizjson = new VizJSON({ + overlays: [{ + type: 'something' + }] + }); + + expect(vizjson.hasOverlay('else')).toBeFalsy(); + }); + }); + + describe('.getOverlayByType', function () { + it("should return the overlay if there's an overlay with the given type", function () { + var vizjson = new VizJSON({ + overlays: [{ + type: 'something' + }] + }); + + expect(vizjson.getOverlayByType('something')).toEqual({ + type: 'something' + }); + }); + + it("should return nothing if there isn't an overlay with the given type", function () { + var vizjson = new VizJSON({ + overlays: [{ + type: 'something' + }] + }); + + expect(vizjson.getOverlayByType('else')).toBeUndefined(); + }); + }); + + describe('.addHeaderOverlay', function () { + it('should add a header Overlay', function () { + var vizjson = new VizJSON({ + url: 'https://carto.com', + title: 'title', + description: 'description' + }); + + vizjson.addHeaderOverlay('show_title', 'show_description', 'is_shareable'); + + expect(vizjson.getOverlayByType('header')).toEqual({ + type: 'header', + order: 1, + shareable: 'is_shareable', + url: 'https://carto.com', + options: { + extra: { + title: 'title', + description: 'description', + show_title: 'show_title', + show_description: 'show_description' + } + } + }); + }); + }); + + describe('.addSearchOverlay', function () { + it('should add a search overlay', function () { + var vizjson = new VizJSON({}); + + vizjson.addSearchOverlay(); + + expect(vizjson.getOverlayByType('search')).toEqual({ + type: 'search', + order: 3 + }); + }); + }); + + describe('.removeOverlay', function () { + it('should remove the overlay of the given type', function () { + var vizjson = new VizJSON({ + overlays: [{ + type: 'something' + }] + }); + + expect(vizjson.getOverlayByType('something')).toBeDefined(); + + vizjson.removeOverlay('something'); + + expect(vizjson.getOverlayByType('something')).not.toBeDefined(); + }); + }); + + describe('.removeLoaderOverlay', function () { + it('should remove the loader overlay', function () { + var vizjson = new VizJSON({ + overlays: [{ + type: C.OVERLAY_TYPES.LOADER + }] + }); + + expect(vizjson.getOverlayByType(C.OVERLAY_TYPES.LOADER)).toBeDefined(); + + vizjson.removeLoaderOverlay(); + + expect(vizjson.getOverlayByType(C.OVERLAY_TYPES.LOADER)).not.toBeDefined(); + }); + }); + + describe('.removeZoomOverlay', function () { + it('should remove the zoom overlay', function () { + var vizjson = new VizJSON({ + overlays: [{ + type: C.OVERLAY_TYPES.ZOOM + }] + }); + + expect(vizjson.getOverlayByType(C.OVERLAY_TYPES.ZOOM)).toBeDefined(); + + vizjson.removeZoomOverlay(); + + expect(vizjson.getOverlayByType(C.OVERLAY_TYPES.ZOOM)).not.toBeDefined(); + }); + }); + + describe('.removeSearchOverlay', function () { + it('should remove the search overlay', function () { + var vizjson = new VizJSON({ + overlays: [{ + type: C.OVERLAY_TYPES.SEARCH + }] + }); + + expect(vizjson.getOverlayByType(C.OVERLAY_TYPES.SEARCH)).toBeDefined(); + + vizjson.removeSearchOverlay(); + + expect(vizjson.getOverlayByType(C.OVERLAY_TYPES.SEARCH)).not.toBeDefined(); + }); + }); + + describe('.enforceGMapsBaseLayer', function () { + it('should replace the existing base layer by a GMaps one', function () { + var vizjson = new VizJSON({ + map_provider: C.MAP_PROVIDER_TYPES.LEAFLET, + layers: [{ + options: { + type: 'Tiled', + url: 'http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png', + name: 'Positron', + className: 'httpsbasemapscartocdncomlight_nolabelszxypng', + attribution: '© OpenStreetMap contributors © CARTO', + urlTemplate: 'http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png' + } + }] + }); + + vizjson.enforceGMapsBaseLayer('roadmap', { color: 'blue' }); + + expect(vizjson.layers[0]).toEqual({ + options: { + type: 'GMapsBase', + url: 'http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png', + name: 'roadmap', + className: 'httpsbasemapscartocdncomlight_nolabelszxypng', + attribution: '', + urlTemplate: 'http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png', + baseType: 'roadmap', + style: {color: 'blue'} + } + }); + expect(vizjson.map_provider).toEqual(C.MAP_PROVIDER_TYPES.GMAPS); + }); + + it('should NOT replace the existing base layer by a GMaps one if map_provider is not leaflet', function () { + var vizjson = new VizJSON({ + map_provider: 'something', + layers: [{ + options: { + type: 'Tiled', + url: 'http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png', + name: 'Positron', + className: 'httpsbasemapscartocdncomlight_nolabelszxypng', + attribution: '© OpenStreetMap contributors © CARTO', + urlTemplate: 'http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png' + } + }] + }); + + vizjson.enforceGMapsBaseLayer('roadmap', { color: 'blue' }); + + expect(vizjson.layers[0]).toEqual({ + options: { + type: 'Tiled', + url: 'http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png', + name: 'Positron', + className: 'httpsbasemapscartocdncomlight_nolabelszxypng', + attribution: '© OpenStreetMap contributors © CARTO', + urlTemplate: 'http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png' + } + }); + + expect(vizjson.map_provider).toEqual('something'); + }); + + it('should NOT replace the existing base layer by a GMaps one if the given type is not valid', function () { + var vizjson = new VizJSON({ + map_provider: C.MAP_PROVIDER_TYPES.LEAFLET, + layers: [{ + options: { + type: 'Tiled', + url: 'http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png', + name: 'Positron', + className: 'httpsbasemapscartocdncomlight_nolabelszxypng', + attribution: '© OpenStreetMap contributors © CARTO', + urlTemplate: 'http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png' + } + }] + }); + + vizjson.enforceGMapsBaseLayer('invalid type', { color: 'blue' }); + + expect(vizjson.layers[0]).toEqual({ + options: { + type: 'Tiled', + url: 'http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png', + name: 'Positron', + className: 'httpsbasemapscartocdncomlight_nolabelszxypng', + attribution: '© OpenStreetMap contributors © CARTO', + urlTemplate: 'http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png' + } + }); + + expect(vizjson.map_provider).toEqual(C.MAP_PROVIDER_TYPES.LEAFLET); + }); + }); + + describe('.setZoom', function () { + it('should set a new zoom and unset bounds', function () { + var vizjson = new VizJSON({ + zoom: 'old_zoom', + bounds: 'bounds' + }); + + vizjson.setZoom('new_zoom'); + + expect(vizjson.zoom).toEqual('new_zoom'); + expect(vizjson.bounds).toBeNull(); + }); + }); + + describe('.setCenter', function () { + it('should set a new center and unset bounds', function () { + var vizjson = new VizJSON({ + center: 'old_center', + bounds: 'bounds' + }); + + vizjson.setCenter('new_center'); + + expect(vizjson.center).toEqual('new_center'); + expect(vizjson.bounds).toBeNull(); + }); + }); + + describe('.setBounds', function () { + it('should set bounds', function () { + var vizjson = new VizJSON({ + bounds: 'old_bounds' + }); + + vizjson.setBounds('new_bounds'); + + expect(vizjson.bounds).toEqual('new_bounds'); + }); + }); + + describe('.setVector', function () { + it('should set vector', function () { + var vizjson = new VizJSON({ + vector: true + }); + + vizjson.setVector(false); + + expect(vizjson.vector).toBeFalsy(); + }); + }); +}); diff --git a/test/spec/cartodb.mod.torque.spec.js b/test/spec/cartodb.mod.torque.spec.js new file mode 100644 index 0000000..370e31c --- /dev/null +++ b/test/spec/cartodb.mod.torque.spec.js @@ -0,0 +1,20 @@ +var torque = require('../../src/cartodb.mod.torque'); + +describe('torque', function () { + it('should set a window.torque object', function () { + expect(torque).toBeDefined(); + expect(window.torque).toBe(torque); + }); + + it('should modify the window.cartodb object', function () { + expect(window.cartodb).toEqual(jasmine.any(Object)); + + var cdb = window.cartodb; + expect(cdb.geo).toEqual(jasmine.any(Object)); + + expect(cdb.geo.GMapsTorqueLayerView).toEqual(jasmine.any(Function)); + expect(cdb.geo.LeafletTorqueLayer).toEqual(jasmine.any(Function)); + + expect(cdb.geo.ui).toEqual(jasmine.any(Object)); + }); +}); diff --git a/test/spec/cartodb.spec.js b/test/spec/cartodb.spec.js new file mode 100644 index 0000000..152df82 --- /dev/null +++ b/test/spec/cartodb.spec.js @@ -0,0 +1,147 @@ +/* global cartodb */ +var cdb = require('../../src/cartodb'); + +describe('cartodb.js bundle', function () { + it('should set cartodb object in global namespace', function () { + expect(cdb).toEqual(jasmine.any(Object)); + }); + + it('should have leaflet set', function () { + expect(cdb.L).toEqual(jasmine.any(Object)); + }); + + it('should have jQuery in addition to the defaults', function () { + expect(cartodb.$).toBeDefined(); + expect(window.$).toBeUndefined(); // …but not in global scope though + }); + + describe('shared for cdb object in all bundles', function () { + it('should set cartodb object in global namespace', function () { + expect(window.cdb).toBeDefined(); + expect(window.cartodb).toBeDefined(); + expect(window.cartodb).toBe(window.cdb); + }); + + it('should have common object placeholders', function () { + expect(cdb.core).toEqual(jasmine.any(Object)); + expect(cdb.vis).toEqual(jasmine.any(Object)); + + expect(cdb.geo).toEqual(jasmine.any(Object)); + expect(cdb.geo.ui).toEqual(jasmine.any(Object)); + expect(cdb.geo.geocoder).toEqual(jasmine.any(Object)); + + expect(cdb.ui).toEqual(jasmine.any(Object)); + expect(cdb.ui.common).toEqual(jasmine.any(Object)); + }); + + it('should have expected objects on cdb object', function () { + expect(cdb.core).toEqual(jasmine.any(Object)); + expect(cdb.vis).toEqual(jasmine.any(Object)); + + expect(cdb.vis.Loader).toEqual(jasmine.any(Object)); + expect(cdb.core.Loader).toBe(cdb.vis.Loader); + + expect(cdb.core.Profiler).toEqual(jasmine.any(Function)); + expect(cdb.core.util).toEqual(jasmine.any(Object)); + + expect(cdb.SQL).toEqual(jasmine.any(Function)); + expect(cdb.Promise).toEqual(jasmine.any(Function)); + + expect(cdb.VERSION).toEqual(jasmine.any(String)); + expect(cdb.DEBUG).toEqual(jasmine.any(Boolean)); + expect(cdb.CARTOCSS_VERSIONS).toEqual(jasmine.any(Object)); + expect(cdb.CARTOCSS_DEFAULT_VERSION).toEqual(jasmine.any(String)); + }); + }); + + describe('shared for cdb object in all bundles except for core', function () { + it('should have the commonly used vendor libs defined', function () { + expect(cdb.$).toEqual(jasmine.any(Function)); + expect(cdb.Mustache).toEqual(jasmine.any(Object)); + expect(cdb.Backbone).toEqual(jasmine.any(Object)); + expect(cdb._).toEqual(jasmine.any(Function)); + }); + + it('should have some common objects', function () { + expect(cdb.config).toEqual(jasmine.any(Object)); + expect(cdb.log).toEqual(jasmine.any(Object)); + expect(cdb.errors).toEqual(jasmine.any(Object)); + expect(cdb.templates).toEqual(jasmine.any(Object)); + expect(cdb.decorators).toEqual(jasmine.any(Object)); + expect(cdb.createVis).toEqual(jasmine.any(Function)); + }); + + it('config should contain links variables', function () { + expect(cdb.config.get('cartodb_attributions')).toEqual('© CARTO'); + expect(cdb.config.get('cartodb_logo_link')).toEqual('http://www.carto.com'); + }); + + it('should generate error when error is called', function () { + expect(cdb.config).toBeDefined(); + cdb.config.ERROR_TRACK_ENABLED = true; + cdb.errors.reset([]); + cdb.log.error('this is an error'); + expect(cdb.errors.size()).toEqual(1); + }); + + it('should create a cdb.core with expected model', function () { + expect(cdb.core.Template).toBeDefined(); + expect(cdb.core.TemplateList).toBeDefined(); + expect(cdb.core.Model).toBeDefined(); + expect(cdb.core.View).toBeDefined(); + expect(cdb.core.Loader).toEqual(jasmine.any(Object)); + }); + + it('should create a cdb.decorators', function () { + expect(cdb.decorators).toBeDefined(); + }); + + it('should create a log', function () { + expect(cdb.log).toBeDefined(); + }); + + it('should add templates stuff', function () { + expect(cdb.templates instanceof cdb.core.TemplateList).toBe(true); + }); + + it('should have a cdb.ui.common object', function () { + expect(cdb.ui.common.FullScreen).toEqual(jasmine.any(Function)); + }); + + it('should have a cdb.geo object', function () { + expect(cdb.geo).toEqual(jasmine.any(Object)); + expect(cdb.geo.geocoder).toEqual(jasmine.any(Object)); + expect(cdb.geo.geocoder.YAHOO).toEqual(jasmine.any(Object)); + expect(cdb.geo.geocoder.NOKIA).toEqual(jasmine.any(Object)); + + expect(cdb.geo.TileLayer).toEqual(jasmine.any(Function)); + expect(cdb.geo.GMapsBaseLayer).toEqual(jasmine.any(Function)); + expect(cdb.geo.WMSLayer).toEqual(jasmine.any(Function)); + expect(cdb.geo.PlainLayer).toEqual(jasmine.any(Function)); + expect(cdb.geo.TorqueLayer).toEqual(jasmine.any(Function)); + expect(cdb.geo.CartoDBLayer).toEqual(jasmine.any(Function)); + expect(cdb.geo.Map).toEqual(jasmine.any(Function)); + expect(cdb.geo.MapView).toEqual(jasmine.any(Function)); + }); + + it('should have a cdb.geo.ui object', function () { + expect(cdb.geo.ui.InfowindowModel).toEqual(jasmine.any(Function)); + expect(cdb.geo.ui.Infowindow).toEqual(jasmine.any(Function)); + expect(cdb.geo.ui.Search).toEqual(jasmine.any(Function)); + expect(cdb.geo.ui.TilesLoader).toEqual(jasmine.any(Function)); + expect(cdb.geo.ui.Tooltip).toEqual(jasmine.any(Function)); + }); + + it('should have a cdb.common object', function () { + expect(cdb.geo.common).toEqual(jasmine.any(Object)); + }); + + it('should have a core.vis', function () { + expect(cdb.vis).toEqual(jasmine.any(Object)); + expect(cdb.vis.Loader).toBe(cdb.core.Loader); + + expect(cdb.vis.Vis).toEqual(jasmine.any(Function)); + expect(cdb.vis.INFOWINDOW_TEMPLATE).toEqual(jasmine.any(Object)); + }); + }); +}); diff --git a/test/spec/core/model.spec.js b/test/spec/core/model.spec.js new file mode 100644 index 0000000..55db481 --- /dev/null +++ b/test/spec/core/model.spec.js @@ -0,0 +1,132 @@ +var $ = require('jquery'); +var Model = require('../../../src/core/model'); + +describe('core/model', function () { + var TestModel; + var model; + + beforeEach(function () { + TestModel = Model.extend({ + initialize: function () { + this.initCalled = true; + Model.prototype.initialize.call(this); + }, + url: 'irrelevant.json', + test_method: function () {} + }); + + spyOn(Model.prototype, 'initialize').and.callThrough(); + model = new TestModel(); + }); + + it('should call initialize', function () { + expect(model.initCalled).toBe(true); + expect(Model.prototype.initialize).toHaveBeenCalled(); + }); + + it('should attach save to the element context', function () { + spyOn(model, 'save'); + model.bind('irrelevantEvent', model.save); + model.trigger('irrelevantEvent'); + expect(model.save).toHaveBeenCalled(); + }); + + it('should attach fetch to the element context', function () { + spyOn(model, 'fetch'); + model.bind('irrelevantEvent', model.fetch); + model.trigger('irrelevantEvent'); + expect(model.fetch).toHaveBeenCalled(); + }); + + it('should add the correct response from server', function () { + model.sync = function (method, model, options) { + options.success({ 'response': true }); + }; + model.fetch(); + expect(model.get('response')).toBeTruthy(); + }); + + it("should trigger 'loadModelStarted' event when fetch", function () { + var loadModelStartedSpy = jasmine.createSpy('loadModelStarted'); + model.bind('loadModelStarted', loadModelStartedSpy); + model.fetch(); + expect(loadModelStartedSpy).toHaveBeenCalled(); + }); + + it("should trigger 'loadModelCompleted' event when fetched", function () { + model.sync = function (method, model, options) { + var dfd = $.Deferred(); + options.success({ 'response': true }); + dfd.resolve(); + return dfd.promise(); + }; + var loadModelCompletedSpy = jasmine.createSpy('loadModelCompleted'); + model.bind('loadModelCompleted', loadModelCompletedSpy); + model.fetch(); + expect(loadModelCompletedSpy).toHaveBeenCalled(); + }); + + it("should trigger 'loadModelFailed' event when fetch fails", function () { + model.url = 'irrelevantError.json'; + + model.sync = function (method, model, options) { + var dfd = $.Deferred(); + options.error({ 'response': true }); + return dfd.reject(); + }; + + var loadModelFailedSpy = jasmine.createSpy('loadModelFailed'); + model.bind('loadModelFailed', loadModelFailedSpy); + + model.fetch(); + expect(loadModelFailedSpy).toHaveBeenCalled(); + }); + + it('should retrigger an event when launched on a descendant object', function (done) { + model.child = new TestModel({}); + model.retrigger('cachopo', model.child); + var spy = jasmine.createSpy('spy'); + model.bind('cachopo', spy); + model.child.trigger('cachopo'); + setTimeout(function () { + expect(spy).toHaveBeenCalled(); + done(); + }, 25); + }); + + it("should trigger 'saving' event when save", function () { + var savingSpy = jasmine.createSpy('saving'); + model.bind('saving', savingSpy); + model.save(); + expect(savingSpy).toHaveBeenCalled(); + }); + + it("should trigger 'saved' event when saved", function () { + model.sync = function (method, model, options) { + var dfd = $.Deferred(); + options.success({ 'response': true }); + dfd.resolve(); + return dfd.promise(); + }; + var savedSpy = jasmine.createSpy('saving'); + model.bind('saved', savedSpy); + model.save(); + expect(savedSpy).toHaveBeenCalled(); + }); + + it("should trigger 'errorSaving' event when save fails", function () { + model.url = 'irrelevantError.json'; + + model.sync = function (method, model, options) { + var dfd = $.Deferred(); + options.error({ 'response': true }); + return dfd.reject(); + }; + + var errorSavingSpy = jasmine.createSpy('errorSaving'); + model.bind('errorSaving', errorSavingSpy); + + model.save(); + expect(errorSavingSpy).toHaveBeenCalled(); + }); +}); diff --git a/test/spec/core/sanitize.spec.js b/test/spec/core/sanitize.spec.js new file mode 100644 index 0000000..c7dbfa7 --- /dev/null +++ b/test/spec/core/sanitize.spec.js @@ -0,0 +1,58 @@ +var sanitize = require('../../../src/core/sanitize'); + +describe('core/sanitize', function () { + describe('.html', function () { + describe('when given a HTML', function () { + it('should allow safe HTML', function () { + expect(sanitize.html('test')).toEqual('test'); + expect(sanitize.html('
works
')).toEqual('
works
'); + }); + + it('should remove unsafe stuff', function () { + expect(sanitize.html(' nono')).toEqual(' nono'); + expect(sanitize.html('nono ')).toEqual('nono '); + }); + + it('should allow target attributes for links', function () { + expect(sanitize.html('carto.com')).toEqual('carto.com'); + }); + it('should remove iframe tag', function () { + expect(sanitize.html('no no'); + + view.model.set('sanitizeTemplate', null); + view.render(); + expect(view.$el.html()).toEqual('no no'); + + var customSanitizeSpy = jasmine.createSpy('sanitizeTemplateSpy').and.returnValue('

custom sanitizied result

'); + view.model.set('sanitizeTemplate', customSanitizeSpy); + expect(customSanitizeSpy).toHaveBeenCalledWith('no