diff --git a/.ember-cli b/.ember-cli index ee64cfed2..8c1812cff 100644 --- a/.ember-cli +++ b/.ember-cli @@ -5,5 +5,11 @@ Setting `disableAnalytics` to true will prevent any data from being sent. */ - "disableAnalytics": false + "disableAnalytics": false, + + /** + Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript + rather than JavaScript by default, when a TypeScript version of a given blueprint is available. + */ + "isTypeScriptProject": false } diff --git a/.eslintignore b/.eslintignore index b6c5918c8..d474a40bd 100644 --- a/.eslintignore +++ b/.eslintignore @@ -19,4 +19,7 @@ # ember-try /.node_modules.ember-try/ /bower.json.ember-try -/package.json.ember-try \ No newline at end of file +/npm-shrinkwrap.json.ember-try +/package.json.ember-try +/package-lock.json.ember-try +/yarn.lock.ember-try diff --git a/.eslintrc.js b/.eslintrc.js index 351c1bdd9..84a88e07f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,28 +1,29 @@ -'use strict'; - module.exports = { root: true, - parser: 'babel-eslint', parserOptions: { - ecmaVersion: 2018, + parser: '@babel/eslint-parser', + ecmaVersion: 'latest', sourceType: 'module', - ecmaFeatures: { - legacyDecorators: true - } + requireConfigFile: false, + babelOptions: { + plugins: [ + ['@babel/plugin-proposal-decorators', { decoratorsBeforeExport: true }], + ], + }, }, - plugins: [ - 'ember' - ], + plugins: ['ember', 'decorator-position'], extends: [ 'eslint:recommended', 'airbnb-base', - 'plugin:ember-best-practices/recommended', + 'plugin:ember/recommended', + 'plugin:decorator-position/ember', + 'plugin:prettier/recommended', ], env: { browser: true, }, - globals:{ - '$': true, + globals: { + $: true, d3: true, }, rules: { @@ -34,40 +35,54 @@ module.exports = { 'space-before-function-paren': 0, 'prefer-arrow-callback': 0, 'no-underscore-dangle': 0, - 'camelcase': 0, + camelcase: 0, 'class-methods-use-this': 0, 'max-len': 0, 'no-param-reassign': 0, - 'ember/avoid-leaking-state-in-ember-objects': 0, - 'ember-best-practices/require-dependent-keys': 0, 'no-undef': 0, + 'ember/no-classic-components': 'warn', + 'ember/no-classic-classes': 'warn', + 'ember/no-get': 'warn', + 'ember/no-actions-hash': 'warn', + 'ember/require-tagless-components': 'warn', + 'ember/no-observers': 'warn', + 'ember/classic-decorator-no-classic-methods': 'warn', + 'ember/classic-decorator-hooks': 'warn', + 'ember/no-component-lifecycle-hooks': 'warn', + 'ember/no-empty-glimmer-component-classes': 'warn', + 'ember/require-computed-macros': 'warn', + 'ember/no-computed-properties-in-native-classes': 'warn', + 'ember/no-legacy-test-waiters': 'warn', + 'ember/no-mixins': 'warn', + 'ember/no-new-mixins': 'warn', + 'ember/avoid-leaking-state-in-ember-objects': 'warn', + 'ember/no-incorrect-calls-with-inline-anonymous-functions': 'warn', + 'max-classes-per-file': 0, + 'decorator-position/decorator-position': 0, }, overrides: [ // node files { files: [ - '.eslintrc.js', - '.template-lintrc.js', - 'ember-cli-build.js', - 'testem.js', - 'blueprints/*/index.js', - 'config/**/*.js', - 'lib/*/index.js' + './.eslintrc.js', + './.prettierrc.js', + './.stylelintrc.js', + './.template-lintrc.js', + './ember-cli-build.js', + './testem.js', + './blueprints/*/index.js', + './config/**/*.js', + './lib/*/index.js', + './server/**/*.js', ], parserOptions: { - sourceType: 'script' + sourceType: 'script', }, env: { browser: false, - node: true + node: true, }, - plugins: ['node'], - extends: ['plugin:node/recommended'], - rules: { - // this can be removed once the following is fixed - // https://github.com/mysticatea/eslint-plugin-node/issues/77 - 'node/no-unpublished-require': 'off' - } - } - ] + extends: ['plugin:n/recommended'], + }, + ], }; diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..5c4bfe166 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,47 @@ +name: CI + +on: + push: + branches: + - main + - master + pull_request: {} + +concurrency: + group: ci-${{ github.head_ref || github.ref }} + cancel-in-progress: true + +jobs: + lint: + name: "Lint" + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v3 + - name: Install Node + uses: actions/setup-node@v3 + with: + node-version: 18.x + cache: yarn + - name: Install Dependencies + run: yarn install --frozen-lockfile + - name: Lint + run: yarn lint + + test: + name: "Test" + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v3 + - name: Install Node + uses: actions/setup-node@v3 + with: + node-version: 18.x + cache: yarn + - name: Install Dependencies + run: yarn install --frozen-lockfile + - name: Run Tests + run: yarn test diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1d5272253..fac056e71 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: 16.x + node-version: 18.x - name: install dependencies run: yarn install --frozen-lockfile --non-interactive - name: test diff --git a/.gitignore b/.gitignore index 413c67967..a30d2b21a 100644 --- a/.gitignore +++ b/.gitignore @@ -25,7 +25,13 @@ development.env # ember-try /.node_modules.ember-try/ /bower.json.ember-try +/npm-shrinkwrap.json.ember-try /package.json.ember-try .eslintcache .yalc +/package-lock.json.ember-try +/yarn.lock.ember-try + +# broccoli-debug +/DEBUG/ diff --git a/.nvmrc b/.nvmrc index d9289897d..3c79f30ec 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -16.15.1 +18.16.0 \ No newline at end of file diff --git a/.prettierignore b/.prettierignore index 922165552..4178fd571 100644 --- a/.prettierignore +++ b/.prettierignore @@ -14,8 +14,12 @@ /coverage/ !.* .eslintcache +.lint-todo/ # ember-try /.node_modules.ember-try/ /bower.json.ember-try +/npm-shrinkwrap.json.ember-try /package.json.ember-try +/package-lock.json.ember-try +/yarn.lock.ember-try diff --git a/.prettierrc.js b/.prettierrc.js index 534e6d35a..959bd58ac 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -1,5 +1,10 @@ -'use strict'; - module.exports = { - singleQuote: true, + overrides: [ + { + files: '*.{js,ts}', + options: { + singleQuote: true, + }, + }, + ], }; diff --git a/.stylelintignore b/.stylelintignore new file mode 100644 index 000000000..a0cf71cbd --- /dev/null +++ b/.stylelintignore @@ -0,0 +1,8 @@ +# unconventional files +/blueprints/*/files/ + +# compiled output +/dist/ + +# addons +/.node_modules.ember-try/ diff --git a/.stylelintrc.js b/.stylelintrc.js new file mode 100644 index 000000000..99fbd8cd7 --- /dev/null +++ b/.stylelintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: ['stylelint-config-standard', 'stylelint-prettier/recommended'], +}; diff --git a/.template-lintrc.js b/.template-lintrc.js index f35f61c7b..4a9912677 100644 --- a/.template-lintrc.js +++ b/.template-lintrc.js @@ -1,5 +1,16 @@ -'use strict'; - module.exports = { extends: 'recommended', + rules: { + 'no-inline-styles': false, + 'no-curly-component-invocation': false, + 'no-action': false, + 'no-yield-only': false, + 'require-presentational-children': false, + 'no-invalid-interactive': false, + 'no-unused-block-params': false, + 'no-negated-condition': false, + 'link-href-attributes': false, + 'no-nested-interactive': false, + 'require-input-label': false, + }, }; diff --git a/app/adapters/application.js b/app/adapters/application.js index 2dbb25fdf..d75499ac7 100644 --- a/app/adapters/application.js +++ b/app/adapters/application.js @@ -1,4 +1,3 @@ -import DS from 'ember-data'; +import JSONAPIAdapter from '@ember-data/adapter/json-api'; -export default DS.JSONAPIAdapter.extend({ -}); +export default JSONAPIAdapter.extend({}); diff --git a/app/adapters/bookmark.js b/app/adapters/bookmark.js index 9c79888b6..cbe32f87e 100644 --- a/app/adapters/bookmark.js +++ b/app/adapters/bookmark.js @@ -1 +1,3 @@ -export { default } from 'ember-local-storage/adapters/adapter'; +import LocalAdapter from 'ember-local-storage/adapters/adapter'; + +export default LocalAdapter; diff --git a/app/adapters/commercial-overlay.js b/app/adapters/commercial-overlay.js index 7cef08659..0842cc705 100644 --- a/app/adapters/commercial-overlay.js +++ b/app/adapters/commercial-overlay.js @@ -1,7 +1,7 @@ import { buildSqlUrl } from '../utils/carto'; import CartoGeojsonFeatureAdapter from './carto-geojson-feature'; -const SQL = function(id) { +const SQL = function (id) { return `SELECT * FROM ( SELECT ST_CollectionExtract(ST_Collect(the_geom),3) as the_geom, overlay as id, @@ -13,9 +13,6 @@ const SQL = function(id) { export default CartoGeojsonFeatureAdapter.extend({ urlForFindRecord(id) { - return buildSqlUrl( - SQL(id), - 'geojson', - ); + return buildSqlUrl(SQL(id), 'geojson'); }, }); diff --git a/app/adapters/layer.js b/app/adapters/layer.js index 3219b2eb1..879035664 100644 --- a/app/adapters/layer.js +++ b/app/adapters/layer.js @@ -1,4 +1,3 @@ import ApplicationAdapter from './application'; -export default ApplicationAdapter.extend({ -}); +export default ApplicationAdapter.extend({}); diff --git a/app/adapters/lot.js b/app/adapters/lot.js index 81faa2a21..8df2c31c6 100644 --- a/app/adapters/lot.js +++ b/app/adapters/lot.js @@ -48,7 +48,7 @@ const LotColumnsSQL = [ 'LOWER(zonemap) AS zonemap', ]; -export const cartoQueryTemplate = function(id) { +export const cartoQueryTemplate = function (id) { return `SELECT ${LotColumnsSQL.join(',')}, /* id:${id} */ st_x(st_centroid(the_geom)) as lon, st_y(st_centroid(the_geom)) as lat, @@ -64,9 +64,6 @@ export default CartoGeojsonFeatureAdapter.extend({ return this._super(status, headers, payload, requestData); }, urlForFindRecord(id) { - return buildSqlUrl( - cartoQueryTemplate(id), - 'geojson', - ); + return buildSqlUrl(cartoQueryTemplate(id), 'geojson'); }, }); diff --git a/app/adapters/special-purpose-district.js b/app/adapters/special-purpose-district.js index b2204a457..0a9e1e877 100644 --- a/app/adapters/special-purpose-district.js +++ b/app/adapters/special-purpose-district.js @@ -1,7 +1,7 @@ import { buildSqlUrl } from '../utils/carto'; import CartoGeojsonFeatureAdapter from './carto-geojson-feature'; -const SQL = function(id) { +const SQL = function (id) { return `SELECT cartodb_id as id, cartodb_id, the_geom, @@ -13,9 +13,6 @@ const SQL = function(id) { export default CartoGeojsonFeatureAdapter.extend({ urlForFindRecord(id) { - return buildSqlUrl( - SQL(id), - 'geojson', - ); + return buildSqlUrl(SQL(id), 'geojson'); }, }); diff --git a/app/adapters/special-purpose-subdistrict.js b/app/adapters/special-purpose-subdistrict.js index d0c4bd743..1e1d24502 100644 --- a/app/adapters/special-purpose-subdistrict.js +++ b/app/adapters/special-purpose-subdistrict.js @@ -1,7 +1,7 @@ import { buildSqlUrl } from '../utils/carto'; import CartoGeojsonFeatureAdapter from './carto-geojson-feature'; -const SQL = function(id) { +const SQL = function (id) { return `SELECT cartodb_id as id, cartodb_id, @@ -15,9 +15,6 @@ const SQL = function(id) { export default CartoGeojsonFeatureAdapter.extend({ urlForFindRecord(id) { - return buildSqlUrl( - SQL(id), - 'geojson', - ); + return buildSqlUrl(SQL(id), 'geojson'); }, }); diff --git a/app/adapters/zoning-district.js b/app/adapters/zoning-district.js index a6e87010b..2e0f85a6e 100644 --- a/app/adapters/zoning-district.js +++ b/app/adapters/zoning-district.js @@ -1,7 +1,7 @@ import { buildSqlUrl } from '../utils/carto'; import CartoGeojsonFeatureAdapter from './carto-geojson-feature'; -const SQL = function(id) { +const SQL = function (id) { return `SELECT * FROM ( SELECT ST_CollectionExtract(ST_Collect(the_geom),3) as the_geom, @@ -15,9 +15,6 @@ const SQL = function(id) { export default CartoGeojsonFeatureAdapter.extend({ urlForFindRecord(id) { - return buildSqlUrl( - SQL(id), - 'geojson', - ); + return buildSqlUrl(SQL(id), 'geojson'); }, }); diff --git a/app/adapters/zoning-map-amendment.js b/app/adapters/zoning-map-amendment.js index 2ad874e62..7bd56f0b1 100644 --- a/app/adapters/zoning-map-amendment.js +++ b/app/adapters/zoning-map-amendment.js @@ -1,7 +1,7 @@ import { buildSqlUrl } from '../utils/carto'; import CartoGeojsonFeatureAdapter from './carto-geojson-feature'; -const SQL = function(ulurpno) { +const SQL = function (ulurpno) { return `SELECT the_geom, ulurpno, ulurpno as id, @@ -15,9 +15,6 @@ const SQL = function(ulurpno) { export default CartoGeojsonFeatureAdapter.extend({ urlForFindRecord(id) { - return buildSqlUrl( - SQL(id), - 'geojson', - ); + return buildSqlUrl(SQL(id), 'geojson'); }, }); diff --git a/app/app.js b/app/app.js index ec3e2372f..441bcca0e 100644 --- a/app/app.js +++ b/app/app.js @@ -1,4 +1,5 @@ import Application from '@ember/application'; +import Ember from 'ember'; import Resolver from 'ember-resolver'; import loadInitializers from 'ember-load-initializers'; import 'ember-concurrency-retryable'; diff --git a/app/components/bookmarks/bookmark-button.js b/app/components/bookmarks/bookmark-button.js index ec1d793f3..60081af45 100644 --- a/app/components/bookmarks/bookmark-button.js +++ b/app/components/bookmarks/bookmark-button.js @@ -5,11 +5,9 @@ import { inject as service } from '@ember/service'; export default class BookmarkButton extends Component { bookmarkableModel = null; - @service - store; + @service store; - @service - metrics; + @service metrics; // we don't know what kind of model this is // we only know that it's bookmarkable @@ -44,7 +42,7 @@ export default class BookmarkButton extends Component { }); // GA - this.get('metrics').trackEvent('MatomoTagManager', { + this.metrics.trackEvent('MatomoTagManager', { category: 'Bookmark', action: 'Used Bookmark', name: 'Used Bookmark', @@ -52,8 +50,10 @@ export default class BookmarkButton extends Component { const { bookmarkableModel } = this; - await this.store.createRecord('bookmark', { - bookmark: bookmarkableModel, - }).save(); + await this.store + .createRecord('bookmark', { + bookmark: bookmarkableModel, + }) + .save(); } } diff --git a/app/components/bookmarks/types/-default.js b/app/components/bookmarks/types/-default.js index 9e803d2d3..837d192de 100644 --- a/app/components/bookmarks/types/-default.js +++ b/app/components/bookmarks/types/-default.js @@ -4,8 +4,7 @@ import { inject as service } from '@ember/service'; import layout from '../../../templates/components/bookmarks/types/-default'; export default class DefaultBookmark extends Component { - @service - metrics; + @service metrics; layout = layout; @@ -20,7 +19,7 @@ export default class DefaultBookmark extends Component { event_category: 'Bookmark', event_action: 'Deleted Bookmark', }); - this.get('metrics').trackEvent('MatomoTagManager', { + this.metrics.trackEvent('MatomoTagManager', { category: 'Bookmark', action: 'Delete', name: 'Deleted Bookmark', @@ -35,7 +34,7 @@ export default class DefaultBookmark extends Component { }); // GA - this.get('metrics').trackEvent('MatomoTagManager', { + this.metrics.trackEvent('MatomoTagManager', { category: 'Download', action: `Downloaded Bookmark as ${format}`, name: 'Export', diff --git a/app/components/bookmarks/types/commercial-overlay.js b/app/components/bookmarks/types/commercial-overlay.js index 27db1c321..edcb81e78 100644 --- a/app/components/bookmarks/types/commercial-overlay.js +++ b/app/components/bookmarks/types/commercial-overlay.js @@ -1 +1,3 @@ -export { default } from './-default'; +import DefaultBookmark from './-default'; + +export default DefaultBookmark; diff --git a/app/components/bookmarks/types/lot.js b/app/components/bookmarks/types/lot.js index 27db1c321..edcb81e78 100644 --- a/app/components/bookmarks/types/lot.js +++ b/app/components/bookmarks/types/lot.js @@ -1 +1,3 @@ -export { default } from './-default'; +import DefaultBookmark from './-default'; + +export default DefaultBookmark; diff --git a/app/components/bookmarks/types/special-purpose-district.js b/app/components/bookmarks/types/special-purpose-district.js index 27db1c321..edcb81e78 100644 --- a/app/components/bookmarks/types/special-purpose-district.js +++ b/app/components/bookmarks/types/special-purpose-district.js @@ -1 +1,3 @@ -export { default } from './-default'; +import DefaultBookmark from './-default'; + +export default DefaultBookmark; diff --git a/app/components/bookmarks/types/special-purpose-subdistrict.js b/app/components/bookmarks/types/special-purpose-subdistrict.js index 27db1c321..edcb81e78 100644 --- a/app/components/bookmarks/types/special-purpose-subdistrict.js +++ b/app/components/bookmarks/types/special-purpose-subdistrict.js @@ -1 +1,3 @@ -export { default } from './-default'; +import DefaultBookmark from './-default'; + +export default DefaultBookmark; diff --git a/app/components/bookmarks/types/zoning-district.js b/app/components/bookmarks/types/zoning-district.js index 27db1c321..edcb81e78 100644 --- a/app/components/bookmarks/types/zoning-district.js +++ b/app/components/bookmarks/types/zoning-district.js @@ -1 +1,3 @@ -export { default } from './-default'; +import DefaultBookmark from './-default'; + +export default DefaultBookmark; diff --git a/app/components/bookmarks/types/zoning-map-amendment.js b/app/components/bookmarks/types/zoning-map-amendment.js index 27db1c321..edcb81e78 100644 --- a/app/components/bookmarks/types/zoning-map-amendment.js +++ b/app/components/bookmarks/types/zoning-map-amendment.js @@ -1 +1,3 @@ -export { default } from './-default'; +import DefaultBookmark from './-default'; + +export default DefaultBookmark; diff --git a/app/components/carto-data-provider.js b/app/components/carto-data-provider.js index ebbc87aff..caa5d5da3 100644 --- a/app/components/carto-data-provider.js +++ b/app/components/carto-data-provider.js @@ -1,18 +1,17 @@ import Component from '@ember/component'; -import { keepLatestTask } from 'ember-concurrency-decorators'; +import { keepLatestTask } from 'ember-concurrency'; import { inject as service } from '@ember/service'; import { computed } from '@ember/object'; import { DelayPolicy } from 'ember-concurrency-retryable'; -import DS from 'ember-data'; +import AdapterError from '@ember-data/adapter/error'; const delayRetryPolicy = new DelayPolicy({ delay: [1000, 2000], - reasons: [DS.AdapterError], + reasons: [AdapterError], }); export default class CartoDataProvider extends Component { - @service - store; + @service store; modelName = 'carto-geojson-feature'; @@ -23,7 +22,7 @@ export default class CartoDataProvider extends Component { return yield this.store.findRecord(this.modelName, this.modelId); }; - @computed('modelName', 'modelId') + @computed('findRecordTask', 'modelId', 'modelName') get taskInstance() { return this.findRecordTask.perform(); } diff --git a/app/components/grouped-checkboxes.js b/app/components/grouped-checkboxes.js index d81f1062b..5d2715350 100644 --- a/app/components/grouped-checkboxes.js +++ b/app/components/grouped-checkboxes.js @@ -9,29 +9,27 @@ export default class GroupedCheckboxesComponent extends Component { group; - selectionChanged = () => {} + selectionChanged = () => {}; @computed('group.codes.length') get hasMany() { - return (this.get('group.codes.length') > 1); + return this.get('group.codes.length') > 1; } - @intersect('selected', 'group.codes') - selectedInGroup; + @intersect('selected', 'group.codes') selectedInGroup; @computed('selectedInGroup', 'group', 'selected') get isIndeterminateGroup() { - const { - selectedInGroup, - group, - } = this; + const { selectedInGroup, group } = this; - return (selectedInGroup.length > 0) && (selectedInGroup.length < group.codes.length); + return ( + selectedInGroup.length > 0 && selectedInGroup.length < group.codes.length + ); } @action addOrRemoveMultiple(needles, haystack) { - if (haystack.any(hay => needles.includes(hay))) { + if (haystack.some((hay) => needles.includes(hay))) { haystack.removeObjects(needles); } else { haystack.pushObjects(needles); diff --git a/app/components/intersecting-layers.js b/app/components/intersecting-layers.js index 176aa5bce..ea1afd86b 100644 --- a/app/components/intersecting-layers.js +++ b/app/components/intersecting-layers.js @@ -1,10 +1,10 @@ -import Component from '@ember/component'; +import Component from '@glimmer/component'; import { get, computed } from '@ember/object'; import RSVP from 'rsvp'; import { task } from 'ember-concurrency'; import carto from '../utils/carto'; -const generateSQL = function(table, bbl) { +const generateSQL = function (table, bbl) { // special handling for tables where we don't want to SELECT * let intersectionTable = table; if (table === 'floodplain_firm2007') { @@ -31,48 +31,63 @@ const generateSQL = function(table, bbl) { }; export default class IntersectingLayersComponent extends Component { - responseIdentifier = 'intersects'; + responseIdentifier = 'intersects'; - bbl = null; + bbl = null; - @task(function* (tables, bbl, responseIdentifier) { - const hash = {}; + @task + *calculateIntersections(tables, bbl, responseIdentifier) { + const hash = {}; - tables.forEach((table) => { - hash[table] = carto.SQL(generateSQL(table, bbl)) - .then((response => get(response[0] || {}, responseIdentifier))); - }); + tables.forEach((table) => { + hash[table] = carto + .SQL(generateSQL(table, bbl)) + .then((response) => get(response[0] || {}, responseIdentifier)); + }); - return yield RSVP.hash(hash); - }) - calculateIntersections; - - willDestroyElement() { - this.get('calculateIntersections').cancelAll(); - } + return yield RSVP.hash(hash); + } - willUpdate() { - this.get('calculateIntersections').cancelAll(); - } + // todo use render modifiers + willDestroyElement(...args) { + super.willDestroyElement(...args); + this.calculateIntersections.cancelAll(); + } - @computed('tables.@each', 'bbl', 'responseIdentifier') - get intersectingLayers() { - const { tables, bbl, responseIdentifier } = this.getProperties('tables', 'bbl', 'responseIdentifier'); - return this.get('calculateIntersections').perform(tables, bbl, responseIdentifier); - } + willUpdate(...args) { + super.willUpdate(...args); + this.calculateIntersections.cancelAll(); + } - @computed('intersectingLayers.value') - get numberIntersecting() { - const intersectingLayers = this.get('intersectingLayers.value'); + @computed( + 'args', + 'bbl', + 'calculateIntersections', + 'responseIdentifier', + 'tables.[]' + ) + get intersectingLayers() { + const { tables, bbl } = this.args; + + return this.calculateIntersections.perform( + tables, + bbl, + this.responseIdentifier + ); + } - if (intersectingLayers) { - const truthyValues = Object - .values(intersectingLayers) - .filter(val => val); + @computed('intersectingLayers.value') + get numberIntersecting() { + const intersectingLayers = this.intersectingLayers.value; - return get(truthyValues, 'length'); - } + if (intersectingLayers) { + const truthyValues = Object.values(intersectingLayers).filter( + (val) => val + ); - return 0; + return truthyValues.length; } + + return 0; + } } diff --git a/app/components/labs-map.js b/app/components/labs-map.js index ed92bd4ac..4c726baaa 100644 --- a/app/components/labs-map.js +++ b/app/components/labs-map.js @@ -1,6 +1,5 @@ import mapboxGlMap from 'ember-mapbox-gl/components/mapbox-gl'; import { assign } from '@ember/polyfills'; -import { get } from '@ember/object'; import { computed, action } from '@ember/object'; import layout from '../templates/components/labs-map'; @@ -24,7 +23,6 @@ const highlightedCircleFeatureLayer = { }, }; - const highlightedLineFeatureLayer = { id: 'highlighted-feature-line', source: 'hovered-feature', @@ -68,14 +66,12 @@ export default class LabsMap extends mapboxGlMap { super.init(...args); // if layerGroups are passed to the map, use the style from that - if (this.get('layerGroups')) { + if (this.layerGroups) { const { - meta: { - mapboxStyle - } - } = this.get('layerGroups') || {}; + meta: { mapboxStyle }, + } = this.layerGroups || {}; - if (mapboxStyle) assign(get(this, 'initOptions') || {}, { style: mapboxStyle }); + if (mapboxStyle) assign(this.initOptions || {}, { style: mapboxStyle }); } } @@ -83,7 +79,7 @@ export default class LabsMap extends mapboxGlMap { @computed('hoveredFeature') get hoveredFeatureSource() { - const feature = this.get('hoveredFeature'); + const feature = this.hoveredFeature; return { type: 'geojson', @@ -92,7 +88,9 @@ export default class LabsMap extends mapboxGlMap { } hoveredFeature = null; + highlightedCircleFeatureLayer = highlightedCircleFeatureLayer; + highlightedLineFeatureLayer = highlightedLineFeatureLayer; /** @@ -111,8 +109,7 @@ export default class LabsMap extends mapboxGlMap { @action _onLoad(map) { // add source for highlighted-feature - map - .addSource('hovered-feature', this.get('hoveredFeatureSource')); + map.addSource('hovered-feature', this.hoveredFeatureSource); map.addLayer(this.highlightedLineFeatureLayer); map.addLayer(this.highlightedCircleFeatureLayer); diff --git a/app/components/labs-ui-overrides/layer-group-toggle.js b/app/components/labs-ui-overrides/layer-group-toggle.js index 92bbb308c..865130267 100644 --- a/app/components/labs-ui-overrides/layer-group-toggle.js +++ b/app/components/labs-ui-overrides/layer-group-toggle.js @@ -5,11 +5,9 @@ import { inject as service } from '@ember/service'; import layout from '../../templates/components/labs-ui-overrides/layer-group-toggle'; export default class LayerGroupToggle extends Component { - @service - fastboot; + @service fastboot; - @service - metrics; + @service metrics; // ember component class options classNames = ['layer-group-toggle']; @@ -19,13 +17,14 @@ export default class LayerGroupToggle extends Component { layout = layout; init(...args) { - super.init(...args); + super.init(args); - this.get('didInit')(this); + this.didInit(this); } - willDestroy() { - this.get('willDestroyHook')(this); + willDestroy(...args) { + super.willDestroy(args); + this.willDestroyHook(this); } // main layerGroup @@ -42,20 +41,15 @@ export default class LayerGroupToggle extends Component { // property bindings from the layer group // includes: label, tooltip, infolink, icon, active - @alias('layerGroup.legend.label') - label = ''; + @alias('layerGroup.legend.label') label = ''; - @alias('layerGroup.legend.tooltip') - tooltip = ''; + @alias('layerGroup.legend.tooltip') tooltip = ''; - @alias('layerGroup.legend.infolink') - infolink = ''; + @alias('layerGroup.legend.infolink') infolink = ''; - @alias('layerGroup.legend.icon') - icon = []; + @alias('layerGroup.legend.icon') icon = []; - @alias('layerGroup.visible') - active = true; + @alias('layerGroup.visible') active = true; // additional options infoLinkIcon = 'external-link-alt'; @@ -88,7 +82,7 @@ export default class LayerGroupToggle extends Component { }); // GA - this.get('metrics').trackEvent('MatomoTagManager', { + this.metrics.trackEvent('MatomoTagManager', { category: 'External Link', action: 'Clicked Supporting Zoning Link', name: `Clicked ${label} Link`, diff --git a/app/components/layer-control-timeline.js b/app/components/layer-control-timeline.js index d74e4a0df..0d2efa80b 100644 --- a/app/components/layer-control-timeline.js +++ b/app/components/layer-control-timeline.js @@ -1,12 +1,12 @@ import Component from '@ember/component'; -import { ChildMixin } from 'ember-composability-tools'; +import { action } from '@ember/object'; const defaultMax = new Date(); const defaultStart = [220924800, defaultMax.getTime()]; function formatDate(date) { const d = new Date(date); - let month = `${(d.getMonth() + 1)}`; + let month = `${d.getMonth() + 1}`; const year = d.getFullYear(); if (month.length < 2) month = `0${month}`; @@ -14,29 +14,32 @@ function formatDate(date) { return [year, month].join('-'); } -export default Component.extend(ChildMixin, { - format: { - to: number => formatDate(number), - from: number => formatDate(number), - }, +export default class LayerControlTimelineComponent extends Component { + layerGroup; - column: '', - start: defaultStart, // epoch time - min: defaultStart[0], - max: defaultStart[1], + column = ''; - actions: { - sliderChanged(value) { - const [min, max] = value; - const { layerGroup, column } = this; + format = { + to: (number) => formatDate(number), + from: (number) => formatDate(number), + }; - this.set('start', value); + start = defaultStart; - const expression = ['all', ['>=', column, min], ['<=', column, max]]; + min = defaultStart[0]; - layerGroup.layerIds.forEach((id) => { - layerGroup.setFilterForLayer(id, expression); - }); - }, - }, -}); + max = defaultStart[1]; + + @action + sliderChanged(value) { + const [min, max] = value; + const { layerGroup, column } = this; + this.set('start', value); + + const expression = ['all', ['>=', column, min], ['<=', column, max]]; + + layerGroup.layerIds.forEach((id) => { + layerGroup.setFilterForLayer(id, expression); + }); + } +} diff --git a/app/components/layer-palette.js b/app/components/layer-palette.js index 68bd0fa94..7a4e6ae41 100644 --- a/app/components/layer-palette.js +++ b/app/components/layer-palette.js @@ -15,11 +15,9 @@ const { @classNames('layer-palette') export default class LayerPaletteComponent extends Component { - @service - metrics; + @service metrics; - @service - fastboot; + @service fastboot; init(...args) { super.init(...args); @@ -31,8 +29,7 @@ export default class LayerPaletteComponent extends Component { this.setFilterForCouncilDistricts(); } - @service - mainMap + @service mainMap; zoomWarningLabel = 'Some information may not be visible at this zoom level.'; @@ -70,20 +67,25 @@ export default class LayerPaletteComponent extends Component { handleLayerGroupChange = () => {}; - toggled = () => this.cityCouncilToggled = !this.cityCouncilToggled; + toggled = () => { + this.cityCouncilToggled = !this.cityCouncilToggled; + }; @action setFilterForZoning() { const expression = [ 'any', - ...this.selectedZoning.map(value => ['==', 'primaryzone', value]), + ...this.selectedZoning.map((value) => ['==', 'primaryzone', value]), ]; // if-guard to prevent the node-based fastboot server from running this // mapbox-gl method which gets ignored in fastboot. if (!this.fastboot.isFastBoot) { next(() => { - this.layerGroups['zoning-districts'].setFilterForLayer('zd-fill', expression); + this.layerGroups['zoning-districts'].setFilterForLayer( + 'zd-fill', + expression + ); }); } } @@ -92,14 +94,20 @@ export default class LayerPaletteComponent extends Component { setFilterForOverlays() { const expression = [ 'any', - ...this.selectedOverlays.map(value => ['==', 'overlay', value]), + ...this.selectedOverlays.map((value) => ['==', 'overlay', value]), ]; // if-guard to prevent the node-based fastboot server from running this // mapbox-gl method which gets ignored in fastboot. if (!this.fastboot.isFastBoot) { next(() => { - this.layerGroups['commercial-overlays'].setFilterForLayer('co', expression); - this.layerGroups['commercial-overlays'].setFilterForLayer('co_labels', expression); + this.layerGroups['commercial-overlays'].setFilterForLayer( + 'co', + expression + ); + this.layerGroups['commercial-overlays'].setFilterForLayer( + 'co_labels', + expression + ); }); } } @@ -108,7 +116,7 @@ export default class LayerPaletteComponent extends Component { setFilterForCouncilDistricts() { const expression = [ 'any', - ...this.selectedCouncilDistricts.map(value => ['==', 'year', value]), + ...this.selectedCouncilDistricts.map((value) => ['==', 'year', value]), ]; this.toggled(); @@ -116,9 +124,18 @@ export default class LayerPaletteComponent extends Component { // mapbox-gl method which gets ignored in fastboot. if (!this.fastboot.isFastBoot) { next(() => { - this.layerGroups['nyc-council-districts-combined'].setFilterForLayer('dcp_city_council_districts_combined-line-glow', expression); - this.layerGroups['nyc-council-districts-combined'].setFilterForLayer('dcp_city_council_districts_combined-line', expression); - this.layerGroups['nyc-council-districts-combined'].setFilterForLayer('dcp_city_council_districts_combined-label', expression); + this.layerGroups['nyc-council-districts-combined'].setFilterForLayer( + 'dcp_city_council_districts_combined-line-glow', + expression + ); + this.layerGroups['nyc-council-districts-combined'].setFilterForLayer( + 'dcp_city_council_districts_combined-line', + expression + ); + this.layerGroups['nyc-council-districts-combined'].setFilterForLayer( + 'dcp_city_council_districts_combined-label', + expression + ); }); } } @@ -127,14 +144,17 @@ export default class LayerPaletteComponent extends Component { setFilterForFirm() { const expression = [ 'any', - ...this.selectedFirm.map(value => ['==', 'fld_zone', value]), + ...this.selectedFirm.map((value) => ['==', 'fld_zone', value]), ]; // if-guard to prevent the node-based fastboot server from running this // mapbox-gl method which gets ignored in fastboot. if (!this.fastboot.isFastBoot) { next(() => { - this.layerGroups['floodplain-efirm2007'].setFilterForLayer('effective-flood-insurance-rate-2007', expression); + this.layerGroups['floodplain-efirm2007'].setFilterForLayer( + 'effective-flood-insurance-rate-2007', + expression + ); }); } } @@ -143,14 +163,17 @@ export default class LayerPaletteComponent extends Component { setFilterForPfirm() { const expression = [ 'any', - ...this.selectedPfirm.map(value => ['==', 'fld_zone', value]), + ...this.selectedPfirm.map((value) => ['==', 'fld_zone', value]), ]; // if-guard to prevent the node-based fastboot server from running this // mapbox-gl method which gets ignored in fastboot. if (!this.fastboot.isFastBoot) { next(() => { - this.layerGroups['floodplain-pfirm2015'].setFilterForLayer('preliminary-flood-insurance-rate', expression); + this.layerGroups['floodplain-pfirm2015'].setFilterForLayer( + 'preliminary-flood-insurance-rate', + expression + ); }); } } @@ -171,14 +194,18 @@ export default class LayerPaletteComponent extends Component { handleLayerGroupToggle(layerGroup) { gtag('event', 'toggle_layer', { event_category: 'Layers', - event_action: `${ layerGroup.visible ? 'Turned on' : 'Turned off' } ${ layerGroup.legend.label }`, + event_action: `${layerGroup.visible ? 'Turned on' : 'Turned off'} ${ + layerGroup.legend.label + }`, }); // GA - this.get('metrics').trackEvent('MatomoTagManager', { + this.metrics.trackEvent('MatomoTagManager', { category: 'Layers', - action: `${ layerGroup.visible ? 'Turned on' : 'Turned off' } ${ layerGroup.legend.label }`, - name: `${ layerGroup.legend.label }`, + action: `${layerGroup.visible ? 'Turned on' : 'Turned off'} ${ + layerGroup.legend.label + }`, + name: `${layerGroup.legend.label}`, }); this.handleLayerGroupChange(); } diff --git a/app/components/layer-record-views/-base.js b/app/components/layer-record-views/-base.js index e852cbd9d..e3bfd1a34 100644 --- a/app/components/layer-record-views/-base.js +++ b/app/components/layer-record-views/-base.js @@ -3,8 +3,7 @@ import { action } from '@ember/object'; import { inject as service } from '@ember/service'; export default class LayerRecordBase extends Component { - @service - metrics; + @service metrics; model = {}; @@ -15,7 +14,7 @@ export default class LayerRecordBase extends Component { event_action: `Clicked ${label} Link`, }); // GA - this.get('metrics').trackEvent('MatomoTagManager', { + this.metrics.trackEvent('MatomoTagManager', { category: 'External Link', action: 'Clicked External Link', name: `Clicked ${label} Link`, diff --git a/app/components/layer-record-views/special-purpose-district.js b/app/components/layer-record-views/special-purpose-district.js index ff9bb917c..6794a82a2 100644 --- a/app/components/layer-record-views/special-purpose-district.js +++ b/app/components/layer-record-views/special-purpose-district.js @@ -1,3 +1,14 @@ +import config from 'labs-zola/config/environment'; import LayerRecordComponent from './-base'; -export default LayerRecordComponent; +const { specialDistrictCrosswalk } = config; + +export default class SpecialPurposeDistrictRecordComponent extends LayerRecordComponent { + get readMoreLink() { + const name = this.model.sdname; + const [, [anchorName, boroName]] = specialDistrictCrosswalk.find( + ([dist]) => dist === name + ) || [[], []]; + return `https://www1.nyc.gov/site/planning/zoning/districts-tools/special-purpose-districts-${boroName}.page#${anchorName}`; + } +} diff --git a/app/components/layer-record-views/special-purpose-subdistrict.js b/app/components/layer-record-views/special-purpose-subdistrict.js index ff9bb917c..236e70d74 100644 --- a/app/components/layer-record-views/special-purpose-subdistrict.js +++ b/app/components/layer-record-views/special-purpose-subdistrict.js @@ -1,3 +1,15 @@ +import config from 'labs-zola/config/environment'; import LayerRecordComponent from './-base'; -export default LayerRecordComponent; +const { specialDistrictCrosswalk } = config; + +export default class SpecialPurposeDistrictRecordComponent extends LayerRecordComponent { + get readMoreLink() { + const name = this.model.sdname; + const [, [anchorName, boroName]] = specialDistrictCrosswalk.find( + ([dist]) => dist === name + ) || [[], []]; + + return `https://www1.nyc.gov/site/planning/zoning/districts-tools/special-purpose-districts-${boroName}.page#${anchorName}`; + } +} diff --git a/app/components/layer-record-views/tax-lot.js b/app/components/layer-record-views/tax-lot.js index ff9bb917c..b6b1d9b8f 100644 --- a/app/components/layer-record-views/tax-lot.js +++ b/app/components/layer-record-views/tax-lot.js @@ -1,3 +1,468 @@ +import carto from 'labs-zola/utils/carto'; +import config from 'labs-zola/config/environment'; import LayerRecordComponent from './-base'; -export default LayerRecordComponent; +const { specialDistrictCrosswalk } = config; + +const specialPurposeDistrictsSQL = function (table, spdist1, spdist2, spdist3) { + return `SELECT DISTINCT sdname, sdlbl FROM ${table} + WHERE sdlbl IN ('${spdist1}', '${spdist2}', '${spdist3}')`; +}; + +const getPrimaryZone = (zonedist = '') => { + if (!zonedist) return ''; + let primary = zonedist.match(/\w\d*/)[0].toLowerCase(); + // special handling for c1 and c2 + if (primary === 'c1' || primary === 'c2') primary = 'c1-c2'; + // special handling for c3 and c3a + if (primary === 'c3' || primary === 'c3a') primary = 'c3-c3a'; + return primary; +}; + +const bldgclassLookup = { + A0: 'One Family Dwellings - Cape Cod', + A1: 'One Family Dwellings - Two Stories Detached (Small or Moderate Size, With or Without Attic)', + A2: 'One Family Dwellings - One Story (Permanent Living Quarters)', + A3: 'One Family Dwellings - Large Suburban Residence', + A4: 'One Family Dwellings - City Residence', + A5: 'One Family Dwellings - Attached or Semi-Detached', + A6: 'One Family Dwellings - Summer Cottages', + A7: 'One Family Dwellings - Mansion Type or Town House', + A8: 'One Family Dwellings - Bungalow Colony/Land Coop Owned', + A9: 'One Family Dwellings - Miscellaneous', + + B1: 'Two Family Dwellings - Brick', + B2: 'Frame', + B3: 'Converted From One Family', + B9: 'Miscellaneous', + + C0: 'Walk-up Apartments - Three Families', + C1: 'Walk-up Apartments - Over Six Families Without Stores', + C2: 'Walk-up Apartments - Five to Six Families', + C3: 'Walk-up Apartments - Four Families', + C4: 'Walk-up Apartments - Old Law Tenements', + C5: 'Walk-up Apartments - Converted Dwelling or Rooming House', + C6: 'Walk-up Apartments - Cooperative', + C7: 'Walk-up Apartments - Over Six Families With Stores', + C8: 'Walk-up Apartments - Co-Op Conversion From Loft/Warehouse', + C9: 'Walk-up Apartments - Garden Apartments', + CM: 'Mobile Homes/Trailer Parks', + + D0: 'Elevator Apartments - Co-op Conversion from Loft/Warehouse', + D1: 'Elevator Apartments - Semi-fireproof (Without Stores)', + D2: 'Elevator Apartments - Artists in Residence', + D3: 'Elevator Apartments - Fireproof (Without Stores)', + D4: 'Elevator Apartments - Cooperatives (Other Than Condominiums)', + D5: 'Elevator Apartments - Converted', + D6: 'Elevator Apartments - Fireproof With Stores', + D7: 'Elevator Apartments - Semi-Fireproof With Stores', + D8: 'Elevator Apartments - Luxury Type', + D9: 'Elevator Apartments - Miscellaneous', + + E1: 'Warehouses - Fireproof', + E2: 'Warehouses - Contractor’s Warehouse', + E3: 'Warehouses - Semi-Fireproof', + E4: 'Warehouses - Frame, Metal', + E7: 'Warehouses - Warehouse, Self Storage', + E9: 'Warehouses - Miscellaneous', + + F1: 'Factory and Industrial Buildings - Heavy Manufacturing - Fireproof', + F2: 'Factory and Industrial Buildings - Special Construction - Fireproof', + F4: 'Factory and Industrial Buildings - Semi-Fireproof', + F5: 'Factory and Industrial Buildings - Light Manufacturing', + F8: 'Factory and Industrial Buildings - Tank Farms', + F9: 'Factory and Industrial Buildings - Miscellaneous', + + G: 'GARAGES AND GASOLINE STATIONS', + G0: 'Residential Tax Class 1 Garage', + G1: 'All Parking Garages', + G2: 'Auto Body/Collision or Auto Repair', + G3: 'Gas Station with Retail Store', + G4: 'Gas Station with Service/Auto Repair', + G5: 'Gas Station only with/without Small Kiosk', + G6: 'Licensed Parking Lot', + G7: 'Unlicensed Parking Lot', + G8: 'Car Sales/Rental with Showroom', + G9: 'Miscellaneous Garage or Gas Station', + GU: 'Car Sales/Rental without Showroom', + GW: 'Car Wash or Lubritorium Facility', + + H1: 'Hotels - Luxury Type', + H2: 'Hotels - Full Service Hotel', + H3: 'Hotels - Limited Service – Many Affiliated with National Chain', + H4: 'Hotels - Motels', + H5: 'Hotels - Private Club, Luxury Type', + H6: 'Hotels - Apartment Hotels', + H7: 'Hotels - Apartment Hotels-Co-op Owned', + H8: 'Hotels - Dormitories', + H9: 'Hotels - Miscellaneous', + HB: 'Hotels - Boutique 10-100 Rooms, with Luxury Facilities, Themed, Stylish, with Full Service Accommodations', + HH: 'Hotels - Hostel-Bed Rental in Dorm Like Setting with Shared Rooms & Bathrooms', + HR: 'Hotels - SRO- 1 or 2 People Housed in Individual Rooms in Multiple Dwelling Affordable Housing', + HS: 'Hotels - Extended Stay/Suite Amenities Similar to Apt., Typically Charge Weekly Rates & Less Expensive than Full Service Hotel', + + I1: 'Hospitals and Health - Hospitals, Sanitariums, Mental Institutions', + I2: 'Hospitals and Health - Infirmary', + I3: 'Hospitals and Health - Dispensary', + I4: 'Hospitals and Health - Staff Facilities', + I5: 'Hospitals and Health - Health Center, Child Center, Clinic', + I6: 'Hospitals and Health - Nursing Home', + I7: 'Hospitals and Health - Adult Care Facility', + I9: 'Hospitals and Health - Miscellaneous', + + J1: 'Theatres - Art Type (Seating Capacity under 400 Seats)', + J2: 'Theatres - Art Type (Seating Capacity Over 400 Seats)', + J3: 'Theatres - Motion Picture Theatre with Balcony', + J4: 'Theatres - Legitimate Theatres (Theatre Sole Use of Building)', + J5: 'Theatres - Theatre in Mixed Use Building', + J6: 'Theatres - T.V. Studios', + J7: 'Theatres - Off-Broadway Type', + J8: 'Theatres - Multiplex Picture Theatre', + J9: 'Theatres - Miscellaneous', + + K1: 'Store Buildings (Taxpayers Included) - One Story Retail Building', + K2: 'Store Buildings (Taxpayers Included) - Multi-Story Retail Building', + K3: 'Store Buildings (Taxpayers Included) - Multi-Story Department Store', + K4: 'Store Buildings (Taxpayers Included) - Predominant Retail with Other Uses', + K5: 'Store Buildings (Taxpayers Included) - Stand Alone Food Establishment', + K6: 'Store Buildings (Taxpayers Included) - Shopping Centers With or Without Parking', + K7: 'Store Buildings (Taxpayers Included) - Banking Facilities with or Without Parking', + K8: 'Store Buildings (Taxpayers Included) - Big Box Retail Not Affixed & Standing On Own Lot with Parking', + K9: 'Store Buildings (Taxpayers Included) - Miscellaneous', + + L1: 'Loft Buildinghs - Over Eight Stores (Mid-Manhattan Type)', + L2: 'Loft Buildinghs - Fireproof and Storage Type (Without Stores)', + L3: 'Loft Buildinghs - Semi-Fireproof', + L8: 'Loft Buildinghs - With Retail Stores Other Than Type 1', + L9: 'Loft Buildinghs - Miscellaneous', + + M1: 'Churches, Synagogues, etc. - Church, Synagogue, Chapel', + M2: 'Churches, Synagogues, etc. - Mission House (Non-Residential)', + M3: 'Churches, Synagogues, etc. - Parsonage, Rectory', + M4: 'Churches, Synagogues, etc. - Convents', + M9: 'Churches, Synagogues, etc. - Miscellaneous', + + N1: 'Asylums and Homes - Asylums', + N2: 'Asylums and Homes - Homes for Indigent Children, Aged, Homeless', + N3: 'Asylums and Homes - Orphanages', + N4: 'Asylums and Homes - Detention House For Wayward Girls', + N9: 'Asylums and Homes - Miscellaneous', + + O1: 'Office Buildings - Office Only – 1 Story', + O2: 'Office Buildings - Office Only – 2-6 Stories', + O3: 'Office Buildings - Office Only – 7-19 Stories', + O4: 'Office Buildings - Office Only or Office with Comm – 20 Stories or More', + O5: 'Office Buildings - Office with Comm – 1 to 6 Stories', + O6: 'Office Buildings - Office with Comm – 7 to 19 Stories', + O7: 'Office Buildings - Professional Buildings/Stand Alone Funeral Homes', + O8: 'Office Buildings - Office with Apartments Only (No Comm)', + O9: 'Office Buildings - Miscellaneous and Old Style Bank Bldgs', + + P1: 'Places of Public Assembly (indoor) and Cultural - Concert Halls', + P2: 'Places of Public Assembly (indoor) and Cultural - Lodge Rooms', + P3: 'Places of Public Assembly (indoor) and Cultural - YWCA, YMCA, YWHA, YMHA, PAL', + P4: 'Places of Public Assembly (indoor) and Cultural - Beach Club', + P5: 'Places of Public Assembly (indoor) and Cultural - Community Center', + P6: 'Places of Public Assembly (indoor) and Cultural - Amusement Place, Bathhouse, Boat House', + P7: 'Places of Public Assembly (indoor) and Cultural - Museum', + P8: 'Places of Public Assembly (indoor) and Cultural - Library', + P9: 'Places of Public Assembly (indoor) and Cultural - Miscellaneous', + + Q0: 'Outdoor Recreation Facilities - Open Space', + Q1: 'Outdoor Recreation Facilities - Parks/Recreation Facilities', + Q2: 'Outdoor Recreation Facilities - Playground', + Q3: 'Outdoor Recreation Facilities - Outdoor Pool', + Q4: 'Outdoor Recreation Facilities - Beach', + Q5: 'Outdoor Recreation Facilities - Golf Course', + Q6: 'Outdoor Recreation Facilities - Stadium, Race Track, Baseball Field', + Q7: 'Outdoor Recreation Facilities - Tennis Court', + Q8: 'Outdoor Recreation Facilities - Marina, Yacht Club', + Q9: 'Outdoor Recreation Facilities - Miscellaneous', + + R0: 'Condominiums - Condo Billing Lot', + R1: 'Condominiums - Residential Unit in 2-10 Unit Bldg', + R2: 'Condominiums - Residential Unit in Walk-Up Bldg', + R3: 'Condominiums - Residential Unit in 1-3 Story Bldg', + R4: 'Condominiums - Residential Unit in Elevator Bldg', + R5: 'Condominiums - Miscellaneous Commercial', + R6: 'Condominiums - Residential Unit of 1-3 Unit Bldg-Orig Class 1', + R7: 'Condominiums - Commercial Unit of 1-3 Units Bldg- Orig Class 1', + R8: 'Condominiums - Commercial Unit of 2-10 Unit Bldg', + R9: 'Condominiums - Co-op within a Condominium', + RA: 'Condominiums - Cultural, Medical, Educational, etc.', + RB: 'Condominiums - Office Space', + RC: 'Condominiums - Commercial Building (Mixed Commercial Condo Building Classification Codes)', + RD: 'Condominiums - Residential Building (Mixed Residential Condo Building Classification Codes)', + RG: 'Condominiums - Indoor Parking', + RH: 'Condominiums - Hotel/Boatel', + RI: 'Condominiums - Mixed Warehouse/Factory/Industrial & Commercial', + RK: 'Condominiums - Retail Space', + RM: 'Condominiums - Mixed Residential & Commercial Building (Mixed Residential & Commercial)', + RP: 'Condominiums - Outdoor Parking', + RR: 'Condominiums - Condominium Rentals', + RS: 'Condominiums - Non-Business Storage Space', + RT: 'Condominiums - Terraces/Gardens/Cabanas', + RW: 'Condominiums - Warehouse/Factory/Industrial', + RX: 'Condominiums - Mixed Residential, Commercial & Industrial', + RZ: 'Condominiums - Mixed Residential & Warehouse', + + S0: 'Residence (Multiple Use) - Primarily One Family with Two Stores or Offices', + S1: 'Residence (Multiple Use) - Primarily One Family with One Store or Office', + S2: 'Residence (Multiple Use) - Primarily Two Family with One Store or Office', + S3: 'Residence (Multiple Use) - Primarily Three Family with One Store or Office', + S4: 'Residence (Multiple Use) - Primarily Four Family with One Store or Office', + S5: 'Residence (Multiple Use) - Primarily Five to Six Family with One Store or Office', + S9: 'Residence (Multiple Use) - Single or Multiple Dwelling with Stores or Offices', + + T1: 'Transportation Facilities (Assessed in ORE) - Airport, Air Field, Terminal', + T2: 'Transportation Facilities (Assessed in ORE) - Pier, Dock, Bulkhead', + T9: 'Transportation Facilities (Assessed in ORE) - Miscellaneous', + + U0: 'Utility Bureau Properties - Utility Company Land and Building', + U1: 'Utility Bureau Properties - Bridge, Tunnel, Highway', + U2: 'Utility Bureau Properties - Gas or Electric Utility', + U3: 'Utility Bureau Properties - Ceiling Railroad', + U4: 'Utility Bureau Properties - Telephone Utility', + U5: 'Utility Bureau Properties - Communications Facilities Other Than Telephone', + U6: 'Utility Bureau Properties - Railroad - Private Ownership', + U7: 'Utility Bureau Properties - Transportation - Public Ownership', + U8: 'Utility Bureau Properties - Revocable Consent', + U9: 'Utility Bureau Properties - Miscellaneous', + + V0: 'Vacant Land - Zoned Residential; Not Manhattan', + V1: 'Vacant Land - Zoned Commercial or Manhattan Residential', + V2: 'Vacant Land - Zoned Commercial Adjacent to Class 1 Dwelling; Not Manhattan', + V3: 'Vacant Land - Zoned Primarily Residential; Not Manhattan', + V4: 'Vacant Land - Police or Fire Department', + V5: 'Vacant Land - School Site or Yard', + V6: 'Vacant Land - Library, Hospital or Museum', + V7: 'Vacant Land - Port Authority of NY and NJ', + V8: 'Vacant Land - New York State & U.S. Government', + V9: 'Vacant Land - Miscellaneous', + + W1: 'Educational Structures - Public Elementary, Junior or Senior High', + W2: 'Educational Structures - Parochial School, Yeshiva', + W3: 'Educational Structures - School or Academy', + W4: 'Educational Structures - Training School', + W5: 'Educational Structures - City University', + W6: 'Educational Structures - Other College and University', + W7: 'Educational Structures - Theological Seminary', + W8: 'Educational Structures - Other Private School', + W9: 'Educational Structures - Miscellaneous', + + Y1: 'Selected Government Installations (Excluding Office Buildings, Training Schools, Academic, Garages, Warehouses, Piers, Air Fields, Vacant Land, Vacant Sites, and Land Under Water and Easements) - Fire Department', + Y2: 'Selected Government Installations (Excluding Office Buildings, Training Schools, Academic, Garages, Warehouses, Piers, Air Fields, Vacant Land, Vacant Sites, and Land Under Water and Easements) - Police Department', + Y3: 'Selected Government Installations (Excluding Office Buildings, Training Schools, Academic, Garages, Warehouses, Piers, Air Fields, Vacant Land, Vacant Sites, and Land Under Water and Easements) - Prison, Jail, House of Detention', + Y4: 'Selected Government Installations (Excluding Office Buildings, Training Schools, Academic, Garages, Warehouses, Piers, Air Fields, Vacant Land, Vacant Sites, and Land Under Water and Easements) - Military and Naval Installation', + Y5: 'Selected Government Installations (Excluding Office Buildings, Training Schools, Academic, Garages, Warehouses, Piers, Air Fields, Vacant Land, Vacant Sites, and Land Under Water and Easements) - Department of Real Estate', + Y6: 'Selected Government Installations (Excluding Office Buildings, Training Schools, Academic, Garages, Warehouses, Piers, Air Fields, Vacant Land, Vacant Sites, and Land Under Water and Easements) - Department of Sanitation', + Y7: 'Selected Government Installations (Excluding Office Buildings, Training Schools, Academic, Garages, Warehouses, Piers, Air Fields, Vacant Land, Vacant Sites, and Land Under Water and Easements) - Department of Ports and Terminals', + Y8: 'Selected Government Installations (Excluding Office Buildings, Training Schools, Academic, Garages, Warehouses, Piers, Air Fields, Vacant Land, Vacant Sites, and Land Under Water and Easements) - Department of Public Works', + Y9: 'Selected Government Installations (Excluding Office Buildings, Training Schools, Academic, Garages, Warehouses, Piers, Air Fields, Vacant Land, Vacant Sites, and Land Under Water and Easements) - Department of Environmental Protection', + + Z0: 'Miscellaneous - Tennis Court, Pool, Shed, etc.', + Z1: 'Miscellaneous - Court House', + Z2: 'Miscellaneous - Public Parking Area', + Z3: 'Miscellaneous - Post Office', + Z4: 'Miscellaneous - Foreign Government', + Z5: 'Miscellaneous - United Nations', + Z7: 'Miscellaneous - Easement', + Z8: 'Miscellaneous - Cemetery', + Z9: 'Miscellaneous - Other', +}; + +const boroughLookup = { + BX: 'Bronx', + BK: 'Brooklyn', + MN: 'Manhattan', + QN: 'Queens', + SI: 'Staten Island', +}; + +const boroLookup = { + 1: 'Manhattan', + 2: 'Bronx', + 3: 'Brooklyn', + 4: 'Queens', + 5: 'Staten Island', +}; + +const ownertypeLookup = { + C: 'City', + M: 'Mixed City & Private', + O: 'Public Authority, State, or Federal', + P: 'Private', + X: 'Mixed', +}; + +const landuseLookup = { + '01': 'One & Two Family Buildings', + '02': 'Multi-Family Walk-Up Buildings', + '03': 'Multi-Family Elevator Buildings', + '04': 'Mixed Residential & Commercial Buildings', + '05': 'Commercial & Office Buildings', + '06': 'Industrial & Manufacturing', + '07': 'Transportation & Utility', + '08': 'Public Facilities & Institutions', + '09': 'Open Space & Outdoor Recreation', + 10: 'Parking Facilities', + 11: 'Vacant Land', +}; + +export default class TaxLotRecordComponent extends LayerRecordComponent { + get bldgclassname() { + return bldgclassLookup[this.model.bldgclass]; + } + + get boroname() { + return boroughLookup[this.model.borough]; + } + + get cdName() { + const borocd = this.model.cd; + const cdborocode = `${borocd}`.substring(0, 1); + const cd = parseInt(`${borocd}`.substring(1, 3), 10).toString(); + return `${boroLookup[cdborocode]} Community District ${cd}`; + } + + get cdURLSegment() { + const borocd = this.model.cd; + const borocode = this.model.borocode; // eslint-disable-line prefer-destructuring + const cleanBorough = boroLookup[borocode].toLowerCase().replace(/\s/g, '-'); + const cd = parseInt(`${borocd}`.substring(1, 3), 10).toString(); + return `${cleanBorough}/${cd}`; + } + + get landusename() { + return landuseLookup[this.model.landuse]; + } + + get ownertypename() { + return ownertypeLookup[this.model.ownertype]; + } + + get housenum() { + const match = this.model.address.match(/([0-9-]*)\s[0-9A-Za-z\s]*/); + return match ? match[1] : ''; + } + + get street() { + const match = this.model.address.match(/[0-9-]*\s([0-9A-Za-z\s]*)/); + return match ? match[1] : ''; + } + + get paddedZonemap() { + const { zonemap } = this.model; + return `0${zonemap}`.slice(-3); + } + + get primaryzone1() { + const zonedist = this.model.zonedist1; + return getPrimaryZone(zonedist); + } + + get primaryzone2() { + const zonedist = this.model.zonedist2; + return getPrimaryZone(zonedist); + } + + get primaryzone3() { + const zonedist = this.model.zonedist3; + return getPrimaryZone(zonedist); + } + + get primaryzone4() { + const zonedist = this.model.zonedist4; + return getPrimaryZone(zonedist); + } + + get parentSpecialPurposeDistricts() { + const DISTRICT_TOOLS_URL = + 'https://www1.nyc.gov/site/planning/zoning/districts-tools'; + const { spdist1, spdist2, spdist3 } = this.model; + + return carto + .SQL( + specialPurposeDistrictsSQL( + 'dcp_special_purpose_districts', + spdist1, + spdist2, + spdist3 + ) + ) + .then((response) => + response.map((item) => { + const [, [anchorName, boroName]] = specialDistrictCrosswalk.find( + ([dist]) => dist === item.sdname + ); + const specialDistrictLink = `${DISTRICT_TOOLS_URL}/special-purpose-districts-${boroName}.page#${anchorName}`; + + return { + label: item.sdlbl.toUpperCase(), + name: item.sdname, + anchorName, + boroName, + specialDistrictLink, + }; + }) + ); + } + + get biswebLink() { + const BISWEB_HOST = + 'http://a810-bisweb.nyc.gov/bisweb/PropertyBrowseByBBLServlet'; + const { borocode, block, lot } = this.model; + + return `${BISWEB_HOST}?allborough=${borocode}&allblock=${block}&alllot=${lot}&go5=+GO+&requestid=0`; + } + + get fullCommunityDistrictURL() { + return `https://communityprofiles.planning.nyc.gov/${this.cdURLSegment}`; + } + + get zoneDistLinks() { + const primaryZones = { + primaryzone1: this.primaryzone1, + primaryzone2: this.primaryzone2, + primaryzone3: this.primaryzone3, + primaryzone4: this.primaryzone4, + }; + + Object.keys(primaryZones).forEach((key) => { + const value = primaryZones[key]; + primaryZones[ + key + ] = `https://www1.nyc.gov/site/planning/zoning/districts-tools/${value}.page`; + }); + + return { + ...primaryZones, + }; + } + + get digitalTaxMapLink() { + return `http://gis.nyc.gov/taxmap/map.htm?searchType=BblSearch&featureTypeName=EVERY_BBL&featureName=${this.model.bbl}`; + } + + get zoningMapLink() { + return `https://s-media.nyc.gov/agencies/dcp/assets/files/pdf/zoning/zoning-maps/map${this.model.zonemap}.pdf`; + } + + get historicalZoningMapLink() { + return `https://s-media.nyc.gov/agencies/dcp/assets/files/pdf/zoning/zoning-maps/maps${this.paddedZonemap}.pdf`; + } + + get ACRISLink() { + const { borocode, block, lot } = this.model; + return `http://a836-acris.nyc.gov/bblsearch/bblsearch.asp?borough=${borocode}&block=${block}&lot=${lot}`; + } + + get housingInfoLink() { + const { borocode } = this.model; + return `https://hpdonline.hpdnyc.org/Hpdonline/Provide_address.aspx?p1=${borocode}&p2=${this.housenum}&p3=${this.street}&SearchButton=Search`; + } + + get councilLink() { + return `https://council.nyc.gov/district-${this.model.council}/`; + } +} diff --git a/app/components/layer-record-views/zoning-district.js b/app/components/layer-record-views/zoning-district.js index ff9bb917c..c4ab2354b 100644 --- a/app/components/layer-record-views/zoning-district.js +++ b/app/components/layer-record-views/zoning-district.js @@ -1,3 +1,123 @@ import LayerRecordComponent from './-base'; -export default LayerRecordComponent; +const zoningDescriptions = { + m1: 'M1 districts are designated for areas with light industries.', + m2: 'M2 districts occupy the middle ground between light and heavy industrial areas.', + m3: 'M3 districts are designated for areas with heavy industries that generate noise, traffic or pollutants.', + c1: 'C1 districts are mapped along streets that serve local retail needs within residential neighborhoods.', + c2: 'C2 districts are mapped along streets that serve local retail needs within residential neighborhoods.', + c3: 'C3 districts permit waterfront recreational activities, primarily boating and fishing, in areas along the waterfront.', + c4: 'C4 districts are mapped in regional centers where larger stores, theaters and office uses serve a wider region and generate more traffic than neighborhood shopping areas.', + c5: 'C5 districts are intended for commercial areas that require central locations or serve the entire metropolitan region.', + c6: 'C6 districts are intended for commercial areas that require central locations or serve the entire metropolitan region.', + c7: 'C7 districts are specifically designated for large open amusement parks.', + c8: 'C8 districts, bridging commercial and manufacturing uses, provide for automotive and other heavy commercial services that often require large amounts of land.', + p: 'A public park is any park, playground, beach, parkway, or roadway within the jurisdiction and control of the New York City Commissioner of Parks & Recreation. Typically, public parks are not subject to zoning regulations.', + r1: 'R1 districts are leafy, low-density neighborhoods of large, single-family detached homes on spacious lots.', + r2: 'Residential development in R2 districts is limited exclusively to single-family detached houses.', + r2a: 'R2A is a contextual district intended to preserve low-rise neighborhoods characterized by single-family detached homes on lots with a minimum width of 40 feet', + r2x: 'R2X districts allow large single-family detached houses on lots with a minimum width of 30 feet.', + r31: 'R3-1 contextual districts are the lowest density districts that allow semi-detached one- and two-family residences, as well as detached homes', + r32: 'R3-2 districts are general residence districts that allow a variety of housing types, including low-rise attached houses, small multifamily apartment houses, and detached and semi-detached one- and two-family residences.', + r3a: 'Characteristic of many of the city’s older neighborhoods, R3A contextual districts feature modest single- and two-family detached residences on zoning lots as narrow as 25 feet in width.', + r3x: 'R3X contextual districts, mapped extensively in lower-density neighborhoods permit only one- and two-family detached homes on lots that must be at least 35 feet wide.', + r4: 'R4 districts are general residence districts that allow a variety of housing types, including low-rise attached houses, small multifamily apartment houses, and detached and semi-detached one- and two-family residences.', + r41: 'R4-1 contextual districts permit only one- and two-family detached and semi-detached houses.', + r4a: 'R4A contextual districts permit only one- and two-family detached residences characterized by houses with two stories and an attic beneath a pitched roof.', + r4b: 'Primarily a contextual rowhouse district limited to low-rise, one- and two-family attached residences, R4B districts also permit detached and semi-detached buildings.', + r5: 'R5 districts are general residence districts that allow a variety of housing types, including low-rise attached houses, small multifamily apartment houses, and detached and semi-detached one- and two-family residences.', + r5a: 'R5A contextual districts permit only one- and two-family detached residences characterized by houses with two stories and an attic beneath a pitched roof.', + r5b: 'Primarily a contextual rowhouse district limited to low-rise, one- and two-family attached residences, R4B districts also permit detached and semi-detached buildings.', + r5d: 'R5D contextual districts are designed to encourage residential growth along major corridors in auto-dependent areas of the city.', + r6: 'R6 zoning districts are widely mapped in built-up, medium-density areas of the city whose character can range from neighborhoods with a diverse mix of building types and heights to large-scale “tower in the park” developments.', + r6a: 'R6A contextual districts produce high lot coverage, six- to eight-story apartment buildings set at or near the street line designed to be compatible with older buildings in medium-density neighborhoods.', + r6b: 'R6B contextual districts are often traditional row house districts, which preserve the scale and harmonious streetscape of medium-density neighborhoods of four-story attached buildings developed during the 19th century.', + r7: 'R7 zoning districts are medium-density apartment house districts that encourage lower apartment buildings on smaller lots and, on larger lots, taller buildings with less lot coverage.', + r7a: 'R7A contextual districts produce high lot coverage, seven- to nine-story apartment buildings set at or near the street line designed to be compatible with older buildings in medium-density neighborhoods.', + r7b: 'R7B contextual districts generally produce six- to seven-story apartment buildings in medium-density neighborhoods.', + r7d: 'R7D contextual districts promote new medium-density contextual development along transit corridors that range between 10 and 11 stories.', + r7x: 'R7X contextual districts are flexible medium-density districts that generally produce 12- to 14-story buildings.', + r8: 'R8 zoning districts are high-density apartment house districts that encourage mid-rise apartment buildings on smaller lots and, on larger lots, taller buildings with less lot coverage.', + r8a: 'R8A contextual districts are high-density districts designed to produce apartment buildings at heights of roughly twelve to fourteen stories.', + r8b: 'R8B contextual districts are designed to preserve the character and scale of taller rowhouse neighborhoods.', + r8x: 'R8X contextual districts are flexible high-density districts that generally produce 15- to 17-story buildings.', + r9: 'R9 districts are high-density districts that permit a wide range of building types including towers.', + r9a: 'R9A contextual districts are high-density districts designed to produce new buildings between 13 and 17 stories that mimics older, high street wall buildings in high-density neighborhoods.', + r9d: 'R9D contextual districts are high-density districts that permit towers that sit on a contextual base.', + r9x: 'R9X contextual districts are high-density districts designed to produce new buildings between 16 and 20 stories that mimics older, high street wall buildings in high-density neighborhoods.', + r10: 'R10 districts are high-density districts that permit a wide range of building types including towers.', + r10a: 'R10-A contextual districts are high-density districts designed to produce new buildings between 21 and 23 stories that mimics older, high street wall buildings in high-density neighborhoods.', + r10x: 'R10X contextual districts are high-density districts that permit towers that sit on a contextual base.', + bpc: 'The Special Battery Park City District (BPC) was created, in accordance with a master plan, to govern extensive residential and commercial development in an area on the Hudson River close to the business core of Lower Manhattan. The district regulates permitted uses and bulk within three specified areas and establishes special design controls with respect to front building walls, building heights, waterfront design and parking.', +}; + +const zoningAbbr = { + R2A: 'r2a', + R2X: 'r2x', + 'R3-1': 'r31', + 'R3-2': 'r32', + R3A: 'r3a', + R3X: 'r3x', + 'R4-1': 'r41', + R4A: 'r4a', + R4B: 'r4b', + R5A: 'r5a', + R5B: 'r5b', + R5D: 'r5d', + R6A: 'r6a', + R6B: 'r6b', + R7A: 'r7a', + R7B: 'r7b', + R7D: 'r7d', + R7X: 'r7x', + R8A: 'r8a', + R8B: 'r8b', + R8X: 'r8x', + R9A: 'r9a', + R9D: 'r9d', // R9D does not have a route + R9X: 'r9x', + R10A: 'r10a', + R10X: 'r10x', // R10X does not have a route + BPC: 'bpc', +}; + +export default class ZoningDistrictRecordComponent extends LayerRecordComponent { + get primaryzone() { + const { zonedist } = this.model; + // convert R6A to r6 + const primary = zonedist.match(/\w\d*/)[0].toLowerCase(); + return primary; + } + + get zoneabbr() { + const { zonedist } = this.model; + const abbr = zonedist.match(/\w\d*/)[0].toLowerCase(); + + if (zonedist in zoningAbbr) { + return zoningAbbr[zonedist]; + } + + return abbr; + } + + get description() { + const { zoneabbr } = this; + + return zoningDescriptions[zoneabbr]; + } + + get primaryzoneURL() { + const { primaryzone } = this; + let url = ''; + + if (primaryzone === 'c1' || primaryzone === 'c2') { + url = 'c1-c2'; + } else if (primaryzone === 'c3') { + url = 'c3-c3a'; + } else { + url = primaryzone; + } + + return url; + } +} diff --git a/app/components/layer-record-views/zoning-map-amendment.js b/app/components/layer-record-views/zoning-map-amendment.js index ff9bb917c..74a6e57a4 100644 --- a/app/components/layer-record-views/zoning-map-amendment.js +++ b/app/components/layer-record-views/zoning-map-amendment.js @@ -1,3 +1,14 @@ import LayerRecordComponent from './-base'; -export default LayerRecordComponent; +export default class ZoningMapAmendmentRecordComponent extends LayerRecordComponent { + get effectiveDisplay() { + return import('moment').then(({ default: moment }) => { + const { effective } = this.model; + + if (effective) { + return moment(effective).utc().format('LL'); + } + return 'To be determined'; + }); + } +} diff --git a/app/components/locate-me-mobile.js b/app/components/locate-me-mobile.js index 7467bb885..27f98f5bd 100644 --- a/app/components/locate-me-mobile.js +++ b/app/components/locate-me-mobile.js @@ -5,22 +5,24 @@ import { inject as service } from '@ember/service'; export default class LocateMeMobileComponent extends Component { // feature for mobile users to make button more visible // button attached to geolocate that functions the same as geoLocate - @service - metrics; + @service metrics; findMeDismissed = false; // TODO: let's refactor this action to make it easier to test @action locateMe() { - const geolocateButton = document.querySelectorAll('.mapboxgl-ctrl-geolocate')[0]; + const geolocateButton = document.querySelectorAll( + '.mapboxgl-ctrl-geolocate' + )[0]; if (geolocateButton) { // GA - this.metrics.trackEvent( - 'MatomoTagManager', - { category: 'Map', action: 'Geolocate on Mobile', name: 'Geolocate' }, - ); + this.metrics.trackEvent('MatomoTagManager', { + category: 'Map', + action: 'Geolocate on Mobile', + name: 'Geolocate', + }); geolocateButton.click(); this.set('findMeDismissed', true); diff --git a/app/components/main-header.js b/app/components/main-header.js index 97f41393a..8fa3ebe46 100644 --- a/app/components/main-header.js +++ b/app/components/main-header.js @@ -2,11 +2,9 @@ import Component from '@ember/component'; import { inject as service } from '@ember/service'; export default class MainHeaderComponent extends Component { - @service('print') - printSvc + @service('print') printSvc; - @service() - media + @service() media; bookmarks; } diff --git a/app/components/main-map.js b/app/components/main-map.js index 7f6b6c951..35f646ac2 100644 --- a/app/components/main-map.js +++ b/app/components/main-map.js @@ -14,9 +14,9 @@ const selectedFillLayer = selectedLayers.fill; const selectedLineLayer = selectedLayers.line; // Custom Control -const MeasurementText = function() { }; +const MeasurementText = function () {}; -MeasurementText.prototype.onAdd = function(map) { +MeasurementText.prototype.onAdd = function (map) { this._map = map; this._container = document.createElement('div'); this._container.id = 'measurement-text'; @@ -30,20 +30,15 @@ MeasurementText.prototype.onRemove = function () { @classNames('map-container') export default class MainMap extends Component { - @service - mainMap; + @service mainMap; - @service - metrics; + @service metrics; - @service - store; + @service store; - @service - router; + @service router; - @service('print') - printSvc; + @service('print') printSvc; menuTo = 'layers-menu'; @@ -70,7 +65,7 @@ export default class MainMap extends Component { }); } - @computed('layerGroupsObject') + @computed('layerGroups', 'layerGroupsObject') get mapConfig() { return this.layerGroups; } @@ -78,8 +73,9 @@ export default class MainMap extends Component { @computed('bookmarks.[]') get bookmarkedLotsLayer() { const bookmarks = this.get('bookmarks.[]'); - const lotBookmarks = bookmarks.getEach('bookmark.properties.bbl') - .filter(d => d); // filter out bookmarks with undefined bbl + const lotBookmarks = bookmarks + .getEach('bookmark.properties.bbl') + .filter((d) => d); // filter out bookmarks with undefined bbl const filter = ['match', ['get', 'bbl'], lotBookmarks, true, false]; @@ -96,14 +92,8 @@ export default class MainMap extends Component { 'line-color': 'rgba(0, 25, 160, 1)', 'line-width': { stops: [ - [ - 13, - 1.5, - ], - [ - 15, - 8, - ], + [13, 1.5], + [15, 8], ], }, }, @@ -113,8 +103,7 @@ export default class MainMap extends Component { return lotBookmarks.length > 0 ? layer : null; } - @alias('mainMap.shouldFitBounds') - shouldFitBounds; + @alias('mainMap.shouldFitBounds') shouldFitBounds; @computed('mainMap.selected') get selectedLotSource() { @@ -152,14 +141,18 @@ export default class MainMap extends Component { // GA geoLocateControl.on('trackuserlocationstart', () => { - this.metrics.trackEvent( - 'MatomoTagManager', - { category: 'Map', action: 'Geolocate', name: 'Geolocate' }, - ); + this.metrics.trackEvent('MatomoTagManager', { + category: 'Map', + action: 'Geolocate', + name: 'Geolocate', + }); }); map.addControl(navigationControl, 'top-left'); - map.addControl(new mapboxgl.ScaleControl({ unit: 'imperial' }), 'bottom-left'); + map.addControl( + new mapboxgl.ScaleControl({ unit: 'imperial' }), + 'bottom-left' + ); map.addControl(geoLocateControl, 'top-left'); map.addControl(new MeasurementText(), 'top-left'); @@ -170,27 +163,9 @@ export default class MainMap extends Component { 'highway_name_motorway', ]; - basemapLayersToHide.forEach(layer => map.removeLayer(layer)); - - map.addSource('ee', { - type: 'image', - url: '/img/ht.png', - coordinates: [ - [-74.0030685, 40.7335205], - [-74.0030515, 40.7335205], - [-74.0030515, 40.7335085], - [-74.0030685, 40.7335085], - ], - }); - - map.addLayer({ - id: 'ee', - source: 'ee', - type: 'raster', - minzoom: 17, - }); + basemapLayersToHide.forEach((layer) => map.removeLayer(layer)); - map.on('zoom', function() { + map.on('zoom', function () { mainMap.set('zoom', map.getZoom()); }); } @@ -203,9 +178,9 @@ export default class MainMap extends Component { if (localSource) { if ( - data.dataType === 'source' - && data.isSourceLoaded - && sourceIds.includes(data.sourceId) + data.dataType === 'source' && + data.isSourceLoaded && + sourceIds.includes(data.sourceId) ) { this.set('loading', false); } else { @@ -216,7 +191,7 @@ export default class MainMap extends Component { @action handleLayerClick(feature) { - const highlightedLayerId = this.get('highlightedLayerId'); + const { highlightedLayerId } = this; if (feature) { const { properties } = feature; @@ -233,34 +208,52 @@ export default class MainMap extends Component { cartodb_id, // eslint-disable-line ceqr_num, // eslint-disable-line } = properties; - - if (bbl && !ceqr_num) { // eslint-disable-line + if (bbl && !ceqr_num) { + // eslint-disable-line const { boro, block, lot } = bblDemux(bbl); this.router.transitionTo('map-feature.lot', boro, block, lot); } if (ulurpno) { - this.router.transitionTo('map-feature.zoning-map-amendment', ulurpno, { queryParams: { search: false } }); + this.router.transitionTo( + 'map-feature.zoning-map-amendment', + ulurpno, + { queryParams: { search: false } } + ); } if (zonedist) { - this.router.transitionTo('map-feature.zoning-district', zonedist, { queryParams: { search: false } }); + this.router.transitionTo('map-feature.zoning-district', zonedist, { + queryParams: { search: false }, + }); } if (sdlbl) { - this.router.transitionTo('map-feature.special-purpose-district', cartodb_id, { queryParams: { search: false } }); + this.router.transitionTo( + 'map-feature.special-purpose-district', + cartodb_id, + { queryParams: { search: false } } + ); } if (splbl) { - this.router.transitionTo('map-feature.special-purpose-subdistrict', cartodb_id, { queryParams: { search: false } }); + this.router.transitionTo( + 'map-feature.special-purpose-subdistrict', + cartodb_id, + { queryParams: { search: false } } + ); } if (overlay) { - this.router.transitionTo('map-feature.commercial-overlay', overlay, { queryParams: { search: false } }); + this.router.transitionTo('map-feature.commercial-overlay', overlay, { + queryParams: { search: false }, + }); } if (bbl && ceqr_num) { - this.router.transitionTo('map-feature.e-designation', id, { queryParams: { search: false } }); + this.router.transitionTo('map-feature.e-designation', id, { + queryParams: { search: false }, + }); } } } diff --git a/app/components/map-measurement-tools.js b/app/components/map-measurement-tools.js index 118929a8c..d1453c1ef 100644 --- a/app/components/map-measurement-tools.js +++ b/app/components/map-measurement-tools.js @@ -5,11 +5,9 @@ import { inject as service } from '@ember/service'; import drawStyles from '../layers/draw-styles'; export default class MapMeasurementToolsComponent extends Component { - @service - mainMap; + @service mainMap; - @service - metrics; + @service metrics; measurementUnitType = 'standard'; @@ -38,18 +36,22 @@ export default class MapMeasurementToolsComponent extends Component { }); // GA - this.get('metrics').trackEvent('MatomoTagManager', { + this.metrics.trackEvent('MatomoTagManager', { category: 'Measurement', action: 'Used measurement tool', name: 'Measurement', }); this.set('didStartDraw', true); - const draw = this.get('draw') || await import('mapbox-gl-draw') - .then(({ default: MapboxDraw }) => new MapboxDraw({ - displayControlsDefault: false, - styles: drawStyles, - })); + const draw = + this.draw || + (await import('@mapbox/mapbox-gl-draw').then( + ({ default: MapboxDraw }) => + new MapboxDraw({ + displayControlsDefault: false, + styles: drawStyles, + }) + )); this.set('draw', draw); const drawMode = type === 'line' ? 'draw_line_string' : 'draw_polygon'; const { mainMap } = this; @@ -66,7 +68,7 @@ export default class MapMeasurementToolsComponent extends Component { @action clearDraw() { - const draw = this.get('draw'); + const { draw } = this; const { mainMap } = this; if (mainMap.get('drawMode')) { mainMap.mapInstance.removeControl(draw); @@ -79,7 +81,7 @@ export default class MapMeasurementToolsComponent extends Component { @action handleDrawCreate(e) { - const draw = this.get('draw'); + const { draw } = this; this.set('drawnFeature', e.features[0].geometry); setTimeout(() => { if (!this.mainMap.isDestroyed && !this.mainMap.isDestroying) { @@ -92,7 +94,7 @@ export default class MapMeasurementToolsComponent extends Component { @action async handleMeasurement() { this.set('drawDidRender', true); - const draw = this.get('draw'); + const { draw } = this; // should log both metric and standard display strings for the current drawn feature const { features } = draw.getAll(); @@ -118,7 +120,7 @@ async function calculateMeasurements(feature) { const { default: area } = await import('@turf/area'); const { default: lineDistance } = await import('@turf/line-distance'); - const drawnLength = (lineDistance(feature) * 1000); // meters + const drawnLength = lineDistance(feature) * 1000; // meters const drawnArea = area(feature); // square meters let metricUnits = 'm'; @@ -129,21 +131,25 @@ async function calculateMeasurements(feature) { let standardFormat = '0,0'; let standardMeasurement; - if (drawnLength > drawnArea) { // user is drawing a line + if (drawnLength > drawnArea) { + // user is drawing a line metricMeasurement = drawnLength; - if (drawnLength >= 1000) { // if over 1000 meters, upgrade metric + if (drawnLength >= 1000) { + // if over 1000 meters, upgrade metric metricMeasurement = drawnLength / 1000; metricUnits = 'km'; metricFormat = '0.00'; } standardMeasurement = drawnLength * 3.28084; - if (standardMeasurement >= 5280) { // if over 5280 feet, upgrade standard + if (standardMeasurement >= 5280) { + // if over 5280 feet, upgrade standard standardMeasurement /= 5280; standardUnits = 'mi'; standardFormat = '0.00'; } - } else { // user is drawing a polygon + } else { + // user is drawing a polygon metricUnits = 'm²'; metricFormat = '0,0'; metricMeasurement = drawnArea; @@ -152,13 +158,15 @@ async function calculateMeasurements(feature) { standardFormat = '0,0'; standardMeasurement = drawnArea * 10.7639; - if (drawnArea >= 1000000) { // if over 1,000,000 meters, upgrade metric + if (drawnArea >= 1000000) { + // if over 1,000,000 meters, upgrade metric metricMeasurement = drawnArea / 1000000; metricUnits = 'km²'; metricFormat = '0.00'; } - if (standardMeasurement >= 27878400) { // if over 27878400 sf, upgrade standard + if (standardMeasurement >= 27878400) { + // if over 27878400 sf, upgrade standard standardMeasurement /= 27878400; standardUnits = 'mi²'; standardFormat = '0.00'; @@ -167,7 +175,9 @@ async function calculateMeasurements(feature) { const drawnMeasurements = { metric: `${numeral(metricMeasurement).format(metricFormat)} ${metricUnits}`, - standard: `${numeral(standardMeasurement).format(standardFormat)} ${standardUnits}`, + standard: `${numeral(standardMeasurement).format( + standardFormat + )} ${standardUnits}`, }; return drawnMeasurements; diff --git a/app/components/map-resource-search.js b/app/components/map-resource-search.js index 0f76b27ae..89f3105e5 100644 --- a/app/components/map-resource-search.js +++ b/app/components/map-resource-search.js @@ -4,14 +4,11 @@ import { inject as service } from '@ember/service'; import bblDemux from '../utils/bbl-demux'; export default class MapResourceSearchComponent extends Component { - @service - router; + @service router; - @service - mainMap; + @service mainMap; - @service - metrics; + @service metrics; @action handleLookupSuccess(center, zoom, bbl) { @@ -24,7 +21,7 @@ export default class MapResourceSearchComponent extends Component { }); // GA - this.get('metrics').trackEvent('MatomoTagManager', { + this.metrics.trackEvent('MatomoTagManager', { category: 'Search', action: 'Used BBL Lookup', name: 'Used BBL Lookup', @@ -33,7 +30,7 @@ export default class MapResourceSearchComponent extends Component { const { boro, block, lot } = bblDemux(bbl); this.router.transitionTo('map-feature.lot', boro, block, lot); } else { - this.get('mainMap.mapInstance').flyTo({ center, zoom }); + this.mainMap.mapInstance.flyTo({ center, zoom }); } } @@ -51,12 +48,14 @@ export default class MapResourceSearchComponent extends Component { if (type === 'lot') { // GA // address search maps to all-uppercase addresses whereas bbl lookups map to normal case addresses - if (result.label.split(',')[0] === result.label.split(',')[0].toUpperCase()) { + if ( + result.label.split(',')[0] === result.label.split(',')[0].toUpperCase() + ) { gtag('event', 'search', { event_category: 'Search', event_action: 'Searched by Address', }); - this.get('metrics').trackEvent('MatomoTagManager', { + this.metrics.trackEvent('MatomoTagManager', { category: 'Search', action: 'Searched by Address', name: 'Searched by Address', @@ -66,7 +65,7 @@ export default class MapResourceSearchComponent extends Component { event_category: 'Search', event_action: 'Used BBL Lookup', }); - this.get('metrics').trackEvent('MatomoTagManager', { + this.metrics.trackEvent('MatomoTagManager', { category: 'Search', action: 'Used BBL Lookup', name: 'Used BBL Lookup', @@ -75,12 +74,18 @@ export default class MapResourceSearchComponent extends Component { const { boro, block, lot } = bblDemux(result.bbl); this.set('searchTerms', result.label); - this.router.transitionTo('map-feature.lot', boro, block, lot, { queryParams: { search: true } }); + this.router.transitionTo('map-feature.lot', boro, block, lot, { + queryParams: { search: true }, + }); } if (type === 'zma') { this.set('searchTerms', result.label); - this.router.transitionTo('map-feature.zoning-map-amendment', result.ulurpno, { queryParams: { search: true } }); + this.router.transitionTo( + 'map-feature.zoning-map-amendment', + result.ulurpno, + { queryParams: { search: true } } + ); } if (type === 'zoning-district') { @@ -89,14 +94,16 @@ export default class MapResourceSearchComponent extends Component { event_action: 'Searched by Zoning District', }); // GA - this.get('metrics').trackEvent('MatomoTagManager', { + this.metrics.trackEvent('MatomoTagManager', { category: 'Search', action: 'Searched by Zoning District', name: 'Searched by Zoning District', }); this.set('searchTerms', result.label); - this.router.transitionTo('map-feature.zoning-district', result.label, { queryParams: { search: true } }); + this.router.transitionTo('map-feature.zoning-district', result.label, { + queryParams: { search: true }, + }); } if (type === 'neighborhood') { @@ -110,12 +117,18 @@ export default class MapResourceSearchComponent extends Component { if (type === 'special-purpose-district') { this.set('searchTerms', result.sdname); - this.router.transitionTo('map-feature.special-purpose-district', result.cartodb_id, { queryParams: { search: true } }); + this.router.transitionTo( + 'map-feature.special-purpose-district', + result.cartodb_id, + { queryParams: { search: true } } + ); } if (type === 'commercial-overlay') { this.set('searchTerms', result.label); - this.router.transitionTo('map-feature.commercial-overlay', result.label, { queryParams: { search: true } }); + this.router.transitionTo('map-feature.commercial-overlay', result.label, { + queryParams: { search: true }, + }); } } } diff --git a/app/components/mapbox/basic-map.js b/app/components/mapbox/basic-map.js index 318180f2f..33c4c41f4 100644 --- a/app/components/mapbox/basic-map.js +++ b/app/components/mapbox/basic-map.js @@ -3,7 +3,7 @@ import { action } from '@ember/object'; import { tagName } from '@ember-decorators/component'; import { buildWaiter } from '@ember/test-waiters'; -let waiter = buildWaiter('ember-friendz:friend-waiter'); +const waiter = buildWaiter('ember-friendz:friend-waiter'); /** * @@ -29,7 +29,7 @@ export default class MapboxBasicMapComponent extends Component { initOptions = {}; - mapLoaded = () => {} + mapLoaded = () => {}; mapInstance = null; diff --git a/app/components/mapbox/fit-map-to-all-button.js b/app/components/mapbox/fit-map-to-all-button.js index 7041a41c3..97801a17c 100644 --- a/app/components/mapbox/fit-map-to-all-button.js +++ b/app/components/mapbox/fit-map-to-all-button.js @@ -6,8 +6,7 @@ export default class MapboxFixMapToAllButton extends Component { // should be carto-geojson-model-like model = {}; - @service - mainMap; + @service mainMap; @action fitBounds() { @@ -15,6 +14,6 @@ export default class MapboxFixMapToAllButton extends Component { event_category: 'Fit Map to Districts', }); - this.get('mainMap.setBounds').perform(this.model.bounds); + this.mainMap.setBounds.perform(this.model.bounds); } } diff --git a/app/components/mapbox/load-spinner.js b/app/components/mapbox/load-spinner.js index 060c4453b..9341d7aae 100644 --- a/app/components/mapbox/load-spinner.js +++ b/app/components/mapbox/load-spinner.js @@ -5,13 +5,12 @@ import { timeout, restartableTask } from 'ember-concurrency'; export default class LoadSpinner extends Component { mapInstance = {}; - @restartableTask - loadStateTask = function* () { + @restartableTask loadStateTask = function* () { yield timeout(500); }; @action handleMapLoading() { - this.get('loadStateTask').perform(); + this.loadStateTask.perform(); } } diff --git a/app/components/mapbox/map-feature-renderer.js b/app/components/mapbox/map-feature-renderer.js index 8c4dd19c5..8039c2db4 100644 --- a/app/components/mapbox/map-feature-renderer.js +++ b/app/components/mapbox/map-feature-renderer.js @@ -5,13 +5,13 @@ export default class MapboxMapFeatureRenderer extends Component { // should be carto-feature-like model = {}; - @service - mainMap; + @service mainMap; // this is usually a query param, which comes through a string. shouldFitBounds = true; - didInsertElement() { + didInsertElement(...args) { + super.didInsertElement(...args); this.setSelectedFeature(this.model); if (this.shouldFitBounds) { @@ -21,7 +21,7 @@ export default class MapboxMapFeatureRenderer extends Component { setFitBounds(model) { const { bounds } = model; - this.get('mainMap.setBounds').perform(bounds); + this.mainMap.setBounds.perform(bounds); } setSelectedFeature(model) { diff --git a/app/components/print-view-controls.js b/app/components/print-view-controls.js index 1326950ed..d25f486f1 100644 --- a/app/components/print-view-controls.js +++ b/app/components/print-view-controls.js @@ -5,11 +5,9 @@ import { inject as service } from '@ember/service'; export default class PrintViewControls extends Component { classNames = ['print-view--controls', 'align-middle']; - @service('print') - printSvc; + @service('print') printSvc; - @service - metrics; + @service metrics; widowResize() { return new Promise((resolve) => { @@ -29,7 +27,7 @@ export default class PrintViewControls extends Component { event_action: 'Disabled print view', }); // GA - this.get('metrics').trackEvent('MatomoTagManager', { + this.metrics.trackEvent('MatomoTagManager', { category: 'Print', action: 'Disabled print view', name: 'export', diff --git a/app/components/tooltip-renderer.js b/app/components/tooltip-renderer.js index 231fc824a..a8a787072 100644 --- a/app/components/tooltip-renderer.js +++ b/app/components/tooltip-renderer.js @@ -3,10 +3,10 @@ import { computed } from '@ember/object'; import mustache from 'mustache'; export default class TooltipRenderer extends Component { - @computed('feature', 'template') + @computed('feature.properties', 'template') get renderedText() { const properties = this.get('feature.properties'); - const template = this.get('template'); + const { template } = this; return mustache.render(template, properties); } diff --git a/app/controllers/application.js b/app/controllers/application.js index a55ec3f57..f42ae2e33 100644 --- a/app/controllers/application.js +++ b/app/controllers/application.js @@ -2,21 +2,20 @@ import Controller from '@ember/controller'; import { assign } from '@ember/polyfills'; import { computed, action } from '@ember/object'; import { inject as service } from '@ember/service'; -import QueryParams from 'ember-parachute'; +import QueryParams from '@nycplanning/ember-parachute'; import config from 'labs-zola/config/environment'; const { defaultLayerGroupState, zoningDistrictOptionSets, commercialOverlaysOptionSets, - cityCouncilDistrictsOptionSets, floodplainEfirm2007OptionSets, floodplainPfirm2015OptionSets, } = config; const defaultLayerGroups = defaultLayerGroupState - .filter(layerGroup => layerGroup.visible) - .map(layerGroup => layerGroup.id) + .filter((layerGroup) => layerGroup.visible) + .map((layerGroup) => layerGroup.id) .sort(); const defaultSelectedOverlays = commercialOverlaysOptionSets @@ -39,57 +38,54 @@ const defaultSelectedPfirmOptionSets = floodplainPfirm2015OptionSets .reduce((acc, curr) => acc.concat(curr)) .sort(); -const defaultSelectedCouncilDistricts = ['2013']; +const defaultSelectedCouncilDistricts = ['2013']; // define new query params here: export const mapQueryParams = new QueryParams( - assign( - { - layerGroups: { - defaultValue: defaultLayerGroups, - refresh: true, - as: 'layer-groups', - }, - - selectedZoning: { - defaultValue: defaultSelectedZoningDistricts, - }, - - selectedOverlays: { - defaultValue: defaultSelectedOverlays, - }, - - selectedCouncilDistricts: { - defaultValue: defaultSelectedCouncilDistricts, - }, - - selectedFirm: { - defaultValue: defaultSelectedFirmOptionSets, - }, - - selectedPfirm: { - defaultValue: defaultSelectedPfirmOptionSets, - }, - - 'aerial-year': { - defaultValue: 'aerials-2016', - }, - - // TODO: After merge of params refactor, update print service based on this param. - print: { defaultValue: false }, + assign({ + layerGroups: { + defaultValue: defaultLayerGroups, + refresh: true, + as: 'layer-groups', }, - ), + + selectedZoning: { + defaultValue: defaultSelectedZoningDistricts, + }, + + selectedOverlays: { + defaultValue: defaultSelectedOverlays, + }, + + selectedCouncilDistricts: { + defaultValue: defaultSelectedCouncilDistricts, + }, + + selectedFirm: { + defaultValue: defaultSelectedFirmOptionSets, + }, + + selectedPfirm: { + defaultValue: defaultSelectedPfirmOptionSets, + }, + + 'aerial-year': { + defaultValue: 'aerials-2016', + }, + + // TODO: After merge of params refactor, update print service based on this param. + print: { defaultValue: false }, + }) ); -export default class ApplicationController extends Controller.extend(mapQueryParams.Mixin) { - @service('print') - printSvc; +export default class ApplicationController extends Controller.extend( + mapQueryParams.Mixin +) { + @service('print') printSvc; - @service - fastboot; + @service fastboot; - @service - mainMap; + @service mainMap; // this action extracts query-param-friendly state of layer groups // for various paramable layers @@ -106,15 +102,14 @@ export default class ApplicationController extends Controller.extend(mapQueryPar @action setModelsToDefault() { - this.model.layerGroups.forEach(model => model.rollbackAttributes()); + this.model.layerGroups.forEach((model) => model.rollbackAttributes()); this.handleLayerGroupChange(); } @computed('queryParamsState') get isDefault() { - const state = this.get('queryParamsState') || {}; + const state = this.queryParamsState || {}; const values = Object.values(state); - - return values.isEvery('changed', false); + return values.every(({ changed }) => changed === false); } } diff --git a/app/controllers/bookmarks.js b/app/controllers/bookmarks.js index 1cbf62232..67dff1ed6 100644 --- a/app/controllers/bookmarks.js +++ b/app/controllers/bookmarks.js @@ -11,8 +11,8 @@ export default Controller.extend({ // this gets us in trouble when we need to do // aggregate operations (like filtering) - bookmarksSettled: computedProp('model.[]', function() { - const bookmarks = this.get('model'); + bookmarksSettled: computedProp('model.[]', function () { + const bookmarks = this.model; const promises = bookmarks.mapBy('recordType'); return Promise.all(promises); diff --git a/app/controllers/map-feature.js b/app/controllers/map-feature.js index dce60b881..94e76d289 100644 --- a/app/controllers/map-feature.js +++ b/app/controllers/map-feature.js @@ -1,11 +1,14 @@ import Controller from '@ember/controller'; export default class MapFeatureController extends Controller { - queryParams = [{ - search: { - type: 'boolean', + queryParams = [ + { + search: { + type: 'boolean', + }, }, - }, 'shouldRefresh']; + 'shouldRefresh', + ]; shouldRefresh = false; } diff --git a/app/helpers/carto-download-link.js b/app/helpers/carto-download-link.js index 510e59ecb..ede2740e9 100644 --- a/app/helpers/carto-download-link.js +++ b/app/helpers/carto-download-link.js @@ -2,7 +2,9 @@ import { helper } from '@ember/component/helper'; import { buildSqlUrl } from '../utils/carto'; export function cartoDownloadLink([table, identifier, ids, format]) { - const query = `SELECT * FROM ${table} WHERE ${identifier} IN (${ids.join(',')})`; + const query = `SELECT * FROM ${table} WHERE ${identifier} IN (${ids.join( + ',' + )})`; return `${buildSqlUrl(query, format)}&filename=${table}`; } diff --git a/app/helpers/humanize-dasherized-words.js b/app/helpers/humanize-dasherized-words.js index 8be5dc403..1825e5bea 100644 --- a/app/helpers/humanize-dasherized-words.js +++ b/app/helpers/humanize-dasherized-words.js @@ -4,7 +4,7 @@ import { capitalize } from '@ember/string'; export function humanizeDasherizedWords([phrase]) { return phrase .split('-') - .map(word => capitalize(word)) + .map((word) => capitalize(word)) .join(' '); } diff --git a/app/helpers/sanitize.js b/app/helpers/sanitize.js index 81be6b1c9..ab74fbe4e 100644 --- a/app/helpers/sanitize.js +++ b/app/helpers/sanitize.js @@ -2,11 +2,14 @@ import { helper } from '@ember/component/helper'; import { htmlSafe } from '@ember/template'; export function sanitize([styleObject]) { - return styleObject ? htmlSafe( - Object - .keys(styleObject) - .reduce((acc, key) => acc.concat(`${key}:${styleObject[key]};`), ''), - ) : ''; + return styleObject + ? htmlSafe( + Object.keys(styleObject).reduce( + (acc, key) => acc.concat(`${key}:${styleObject[key]};`), + '' + ) + ) + : ''; } export default helper(sanitize); diff --git a/app/helpers/to-title-case.js b/app/helpers/to-title-case.js index 0911f5169..dc8a65bdc 100644 --- a/app/helpers/to-title-case.js +++ b/app/helpers/to-title-case.js @@ -1,7 +1,10 @@ import { helper } from '@ember/component/helper'; function toTitleCase([str]) { - return str.replace(/\w\S*/g, txt => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase()); + return str.replace( + /\w\S*/g, + (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase() + ); } export default helper(toTitleCase); diff --git a/app/helpers/zoom-dependent-label.js b/app/helpers/zoom-dependent-label.js index abd540588..399534b0d 100644 --- a/app/helpers/zoom-dependent-label.js +++ b/app/helpers/zoom-dependent-label.js @@ -1,16 +1,11 @@ import Helper from '@ember/component/helper'; export default Helper.extend({ - compute([layerGroup, label, mapZoom]) { - const allMinzooms = layerGroup.layers.map((layer) => { - if (layer.style) { - return layer.style.minzoom; - } - return false; - }).filter(zoom => !!zoom); - const maxOfallMinzooms = (allMinzooms.length) ? Math.max(...allMinzooms) : false; - return (mapZoom < maxOfallMinzooms) ? label : null; + const largestMinZoom = layerGroup.get('largestMinZoom'); + if (typeof largestMinZoom !== 'number') { + return null; + } + return mapZoom < largestMinZoom ? label : null; }, - }); diff --git a/app/initializers/route-css-classes.js b/app/initializers/route-css-classes.js index e00283cf8..6dffbc808 100644 --- a/app/initializers/route-css-classes.js +++ b/app/initializers/route-css-classes.js @@ -1,9 +1,9 @@ -import Ember from 'ember'; +import Route from '@ember/routing/route'; import { inject as service } from '@ember/service'; import { dasherize } from '@ember/string'; export function initialize() { - Ember.Route.reopen({ + Route.reopen({ fastboot: service(), activate(...args) { this._super(...args); @@ -34,13 +34,16 @@ export function initialize() { if (el.classList) { el.classList.remove(className); } else { - el.className = el.className.replace(new RegExp(`(^|\\b)${className.split(' ').join('|')}(\\b|$)`, 'gi'), ' '); + el.className = el.className.replace( + new RegExp(`(^|\\b)${className.split(' ').join('|')}(\\b|$)`, 'gi'), + ' ' + ); } } }, getRouteCssClass() { - return `${dasherize(this.get('routeName').replace(/\./g, '-'))}`; + return `${dasherize(this.routeName.replace(/\./g, '-'))}`; }, getBodyElement() { diff --git a/app/layers/draw-styles.js b/app/layers/draw-styles.js index 500d0270c..925616fed 100644 --- a/app/layers/draw-styles.js +++ b/app/layers/draw-styles.js @@ -46,7 +46,12 @@ export default [ { id: 'gl-draw-polygon-and-line-vertex-halo-active', type: 'circle', - filter: ['all', ['==', 'meta', 'vertex'], ['==', '$type', 'Point'], ['!=', 'mode', 'static']], + filter: [ + 'all', + ['==', 'meta', 'vertex'], + ['==', '$type', 'Point'], + ['!=', 'mode', 'static'], + ], paint: { 'circle-radius': 7, 'circle-color': '#FFF', @@ -56,7 +61,12 @@ export default [ { id: 'gl-draw-polygon-and-line-vertex-active', type: 'circle', - filter: ['all', ['==', 'meta', 'vertex'], ['==', '$type', 'Point'], ['!=', 'mode', 'static']], + filter: [ + 'all', + ['==', 'meta', 'vertex'], + ['==', '$type', 'Point'], + ['!=', 'mode', 'static'], + ], paint: { 'circle-radius': 6, 'circle-color': '#D96B27', diff --git a/app/layers/drawn-feature.js b/app/layers/drawn-feature.js index 27b191cbf..511671f0e 100644 --- a/app/layers/drawn-feature.js +++ b/app/layers/drawn-feature.js @@ -7,10 +7,7 @@ export default { 'line-color': 'rgba(62, 35, 234, 1)', 'line-opacity': 0.7, 'line-width': 2, - 'line-dasharray': [ - 5, - 2, - ], + 'line-dasharray': [5, 2], }, }, fill: { diff --git a/app/layers/selected-lot.js b/app/layers/selected-lot.js index 3708c018a..20458a4e5 100644 --- a/app/layers/selected-lot.js +++ b/app/layers/selected-lot.js @@ -20,20 +20,11 @@ const selectedLayers = { 'line-color': 'rgba(0, 10, 90, 1)', 'line-width': { stops: [ - [ - 13, - 1.5, - ], - [ - 15, - 8, - ], + [13, 1.5], + [15, 8], ], }, - 'line-dasharray': [ - 2, - 1.5, - ], + 'line-dasharray': [2, 1.5], }, }, }; diff --git a/app/mixins/track-page.js b/app/mixins/track-page.js index e095c0f2a..94c765a37 100644 --- a/app/mixins/track-page.js +++ b/app/mixins/track-page.js @@ -3,20 +3,21 @@ import Mixin from '@ember/object/mixin'; import { scheduleOnce } from '@ember/runloop'; import { on } from '@ember/object/evented'; -var skipDoubleCountingBecauseThisIsTheInitialPageLoad = true; +let skipDoubleCountingBecauseThisIsTheInitialPageLoad = true; export default Mixin.create({ metrics: service(), - trackPage: on('routeDidChange', function() { + trackPage: on('routeDidChange', function () { this._trackPage(); }), _trackPage() { scheduleOnce('afterRender', this, () => { const page = this.url; - const title = this.getWithDefault('currentRouteName', 'unknown'); - if(skipDoubleCountingBecauseThisIsTheInitialPageLoad) { + const title = this.currentRouteName || 'unknown'; + + if (skipDoubleCountingBecauseThisIsTheInitialPageLoad) { skipDoubleCountingBecauseThisIsTheInitialPageLoad = false; } else { this.metrics.trackPage({ page, title }); diff --git a/app/models/bookmark.js b/app/models/bookmark.js index dd0ad2f33..e174043bc 100644 --- a/app/models/bookmark.js +++ b/app/models/bookmark.js @@ -1,15 +1,14 @@ -import DS from 'ember-data'; +import DS from 'ember-data'; // eslint-disable-line import { computed } from '@ember/object'; +import { resolve } from 'rsvp'; +import ObjectProxy from '@ember/object/proxy'; +import PromiseProxyMixin from '@ember/object/promise-proxy-mixin'; -const { - PromiseObject, - Model, - attr, - belongsTo, -} = DS; +const ObjectPromiseProxy = ObjectProxy.extend(PromiseProxyMixin); +const { Model, attr, belongsTo } = DS; // eslint-disable-line export default class BookmarkModel extends Model { - @belongsTo('bookmark', { inverse: 'bookmark' }) bookmark; + @belongsTo('bookmark', { inverse: 'bookmark', polymorphic: true }) bookmark; @attr('string') address; @@ -17,15 +16,17 @@ export default class BookmarkModel extends Model { @computed('bookmark') get recordType() { - const bookmark = this.get('bookmark'); - return PromiseObject.create({ - promise: bookmark.then((bmark) => { - if (bmark) { - return bmark.get('constructor.modelName'); - } + const { bookmark } = this; + return ObjectPromiseProxy.create({ + promise: resolve( + bookmark.then((bmark) => { + if (bmark) { + return bmark.get('constructor.modelName'); + } - return 'address'; - }), + return 'address'; + }) + ), }); } } diff --git a/app/models/carto-geojson-feature.js b/app/models/carto-geojson-feature.js index c0a1471e9..5d1ab6895 100644 --- a/app/models/carto-geojson-feature.js +++ b/app/models/carto-geojson-feature.js @@ -1,16 +1,12 @@ -import DS from 'ember-data'; +import { attr } from '@ember-data/model'; import bbox from '@turf/bbox'; import { computed } from '@ember/object'; import Bookmarkable from './bookmark'; -const { attr } = DS; - export default class GeoJsonFeatureModel extends Bookmarkable { - @attr() - geometry; + @attr() geometry; - @attr() - properties; + @attr() properties; @attr('string', { defaultValue: 'Polygon', @@ -19,11 +15,9 @@ export default class GeoJsonFeatureModel extends Bookmarkable { // generic property names to be aliased into // from specific models - @attr('string') - title + @attr('string') title; - @attr('string') - subtitle + @attr('string') subtitle; @computed('geometry') get bounds() { diff --git a/app/models/commercial-overlay.js b/app/models/commercial-overlay.js index d73fd1154..96529ad75 100644 --- a/app/models/commercial-overlay.js +++ b/app/models/commercial-overlay.js @@ -1,13 +1,9 @@ -import { - fragment, -} from 'ember-data-model-fragments/attributes'; +import { attr } from '@ember-data/model'; import { alias } from '@ember/object/computed'; import CartoGeojsonFeature from './carto-geojson-feature'; export default class CommercialOverlay extends CartoGeojsonFeature { - @fragment('map-features/commercial-overlay') - properties; + @attr properties; - @alias('properties.overlay') - title; + @alias('properties.overlay') title; } diff --git a/app/models/e-designation.js b/app/models/e-designation.js index 53976d666..eae0f1cf0 100644 --- a/app/models/e-designation.js +++ b/app/models/e-designation.js @@ -1,9 +1,6 @@ -import { - fragment, -} from 'ember-data-model-fragments/attributes'; +import { attr } from '@ember-data/model'; import CartoGeojsonFeature from './carto-geojson-feature'; export default class EDesignation extends CartoGeojsonFeature { - @fragment('map-features/e-designation') - properties; + @attr properties; } diff --git a/app/models/layer.js b/app/models/layer.js index 9b7d96961..5a5396351 100644 --- a/app/models/layer.js +++ b/app/models/layer.js @@ -9,14 +9,14 @@ export default LayerModel.extend({ delegateVisibility() { const visible = this.get('layerGroup.visible'); - if (this.get('layerVisibilityType') === 'singleton') { - if (this.get('position') === 1 && this.get('layerGroup.visible')) { - next(() => (!this.get('isDestroyed') ? this.set('visibility', true) : null)); + if (this.layerVisibilityType === 'singleton') { + if (this.position === 1 && this.get('layerGroup.visible')) { + next(() => (!this.isDestroyed ? this.set('visibility', true) : null)); } else { - next(() => (!this.get('isDestroyed') ? this.set('visibility', false) : null)); + next(() => (!this.isDestroyed ? this.set('visibility', false) : null)); } } else { - next(() => (!this.get('isDestroyed') ? this.set('visibility', visible) : null)); + next(() => (!this.isDestroyed ? this.set('visibility', visible) : null)); } }, }); diff --git a/app/models/lot.js b/app/models/lot.js index 2bc305c21..5e546c866 100644 --- a/app/models/lot.js +++ b/app/models/lot.js @@ -1,16 +1,14 @@ -import { - fragment, -} from 'ember-data-model-fragments/attributes'; -import { alias } from '@ember/object/computed'; +import { attr } from '@ember-data/model'; import CartoGeojsonFeature from './carto-geojson-feature'; export default class Lot extends CartoGeojsonFeature { - @fragment('map-features/lot') - properties; + @attr properties; - @alias('properties.address') - title; + get title() { + return this.get('properties.address'); + } - @alias('properties.bbl') - subtitle; + get subtitle() { + return this.get('properties.bbl'); + } } diff --git a/app/models/map-features/commercial-overlay.js b/app/models/map-features/commercial-overlay.js deleted file mode 100644 index d4452345a..000000000 --- a/app/models/map-features/commercial-overlay.js +++ /dev/null @@ -1,11 +0,0 @@ -import DS from 'ember-data'; -import MF from 'ember-data-model-fragments'; - -const { attr } = DS; - -// this model fragment structures the "properties" -// node of a geojson feature -export default class CommercialOverlayFragment extends MF.Fragment { - @attr('string') - overlay; -} diff --git a/app/models/map-features/e-designation.js b/app/models/map-features/e-designation.js deleted file mode 100644 index df0008759..000000000 --- a/app/models/map-features/e-designation.js +++ /dev/null @@ -1,18 +0,0 @@ -import DS from 'ember-data'; -import MF from 'ember-data-model-fragments'; - -const { attr } = DS; - -export default class EDesignationFragment extends MF.Fragment { - @attr('string') - address; - - @attr('string') - ceqr_num; - - @attr('string') - enumber; - - @attr('string') - ulurp_num; -} diff --git a/app/models/map-features/lot.js b/app/models/map-features/lot.js deleted file mode 100644 index db2108316..000000000 --- a/app/models/map-features/lot.js +++ /dev/null @@ -1,568 +0,0 @@ -import DS from 'ember-data'; -import MF from 'ember-data-model-fragments'; -import { computed } from '@ember/object'; -import { alias } from '@ember/object/computed'; -import carto from 'labs-zola/utils/carto'; -import config from 'labs-zola/config/environment'; - -const { specialDistrictCrosswalk } = config; - -const { attr } = DS; - -const specialPurposeDistrictsSQL = function(table, spdist1, spdist2, spdist3) { - return `SELECT DISTINCT sdname, sdlbl FROM ${table} - WHERE sdlbl IN ('${spdist1}', '${spdist2}', '${spdist3}')`; -}; - -const getPrimaryZone = (zonedist = '') => { - if (!zonedist) return ''; - let primary = zonedist.match(/\w\d*/)[0].toLowerCase(); - // special handling for c1 and c2 - if ((primary === 'c1') || (primary === 'c2')) primary = 'c1-c2'; - // special handling for c3 and c3a - if ((primary === 'c3') || (primary === 'c3a')) primary = 'c3-c3a'; - return primary; -}; - -const bldgclassLookup = { - A0: 'One Family Dwellings - Cape Cod', - A1: 'One Family Dwellings - Two Stories Detached (Small or Moderate Size, With or Without Attic)', - A2: 'One Family Dwellings - One Story (Permanent Living Quarters)', - A3: 'One Family Dwellings - Large Suburban Residence', - A4: 'One Family Dwellings - City Residence', - A5: 'One Family Dwellings - Attached or Semi-Detached', - A6: 'One Family Dwellings - Summer Cottages', - A7: 'One Family Dwellings - Mansion Type or Town House', - A8: 'One Family Dwellings - Bungalow Colony/Land Coop Owned', - A9: 'One Family Dwellings - Miscellaneous', - - B1: 'Two Family Dwellings - Brick', - B2: 'Frame', - B3: 'Converted From One Family', - B9: 'Miscellaneous', - - C0: 'Walk-up Apartments - Three Families', - C1: 'Walk-up Apartments - Over Six Families Without Stores', - C2: 'Walk-up Apartments - Five to Six Families', - C3: 'Walk-up Apartments - Four Families', - C4: 'Walk-up Apartments - Old Law Tenements', - C5: 'Walk-up Apartments - Converted Dwelling or Rooming House', - C6: 'Walk-up Apartments - Cooperative', - C7: 'Walk-up Apartments - Over Six Families With Stores', - C8: 'Walk-up Apartments - Co-Op Conversion From Loft/Warehouse', - C9: 'Walk-up Apartments - Garden Apartments', - CM: 'Mobile Homes/Trailer Parks', - - D0: 'Elevator Apartments - Co-op Conversion from Loft/Warehouse', - D1: 'Elevator Apartments - Semi-fireproof (Without Stores)', - D2: 'Elevator Apartments - Artists in Residence', - D3: 'Elevator Apartments - Fireproof (Without Stores)', - D4: 'Elevator Apartments - Cooperatives (Other Than Condominiums)', - D5: 'Elevator Apartments - Converted', - D6: 'Elevator Apartments - Fireproof With Stores', - D7: 'Elevator Apartments - Semi-Fireproof With Stores', - D8: 'Elevator Apartments - Luxury Type', - D9: 'Elevator Apartments - Miscellaneous', - - E1: 'Warehouses - Fireproof', - E2: 'Warehouses - Contractor’s Warehouse', - E3: 'Warehouses - Semi-Fireproof', - E4: 'Warehouses - Frame, Metal', - E7: 'Warehouses - Warehouse, Self Storage', - E9: 'Warehouses - Miscellaneous', - - F1: 'Factory and Industrial Buildings - Heavy Manufacturing - Fireproof', - F2: 'Factory and Industrial Buildings - Special Construction - Fireproof', - F4: 'Factory and Industrial Buildings - Semi-Fireproof', - F5: 'Factory and Industrial Buildings - Light Manufacturing', - F8: 'Factory and Industrial Buildings - Tank Farms', - F9: 'Factory and Industrial Buildings - Miscellaneous', - - G: 'GARAGES AND GASOLINE STATIONS', - G0: 'Residential Tax Class 1 Garage', - G1: 'All Parking Garages', - G2: 'Auto Body/Collision or Auto Repair', - G3: 'Gas Station with Retail Store', - G4: 'Gas Station with Service/Auto Repair', - G5: 'Gas Station only with/without Small Kiosk', - G6: 'Licensed Parking Lot', - G7: 'Unlicensed Parking Lot', - G8: 'Car Sales/Rental with Showroom', - G9: 'Miscellaneous Garage or Gas Station', - GU: 'Car Sales/Rental without Showroom', - GW: 'Car Wash or Lubritorium Facility', - - H1: 'Hotels - Luxury Type', - H2: 'Hotels - Full Service Hotel', - H3: 'Hotels - Limited Service – Many Affiliated with National Chain', - H4: 'Hotels - Motels', - H5: 'Hotels - Private Club, Luxury Type', - H6: 'Hotels - Apartment Hotels', - H7: 'Hotels - Apartment Hotels-Co-op Owned', - H8: 'Hotels - Dormitories', - H9: 'Hotels - Miscellaneous', - HB: 'Hotels - Boutique 10-100 Rooms, with Luxury Facilities, Themed, Stylish, with Full Service Accommodations', - HH: 'Hotels - Hostel-Bed Rental in Dorm Like Setting with Shared Rooms & Bathrooms', - HR: 'Hotels - SRO- 1 or 2 People Housed in Individual Rooms in Multiple Dwelling Affordable Housing', - HS: 'Hotels - Extended Stay/Suite Amenities Similar to Apt., Typically Charge Weekly Rates & Less Expensive than Full Service Hotel', - - I1: 'Hospitals and Health - Hospitals, Sanitariums, Mental Institutions', - I2: 'Hospitals and Health - Infirmary', - I3: 'Hospitals and Health - Dispensary', - I4: 'Hospitals and Health - Staff Facilities', - I5: 'Hospitals and Health - Health Center, Child Center, Clinic', - I6: 'Hospitals and Health - Nursing Home', - I7: 'Hospitals and Health - Adult Care Facility', - I9: 'Hospitals and Health - Miscellaneous', - - J1: 'Theatres - Art Type (Seating Capacity under 400 Seats)', - J2: 'Theatres - Art Type (Seating Capacity Over 400 Seats)', - J3: 'Theatres - Motion Picture Theatre with Balcony', - J4: 'Theatres - Legitimate Theatres (Theatre Sole Use of Building)', - J5: 'Theatres - Theatre in Mixed Use Building', - J6: 'Theatres - T.V. Studios', - J7: 'Theatres - Off-Broadway Type', - J8: 'Theatres - Multiplex Picture Theatre', - J9: 'Theatres - Miscellaneous', - - K1: 'Store Buildings (Taxpayers Included) - One Story Retail Building', - K2: 'Store Buildings (Taxpayers Included) - Multi-Story Retail Building', - K3: 'Store Buildings (Taxpayers Included) - Multi-Story Department Store', - K4: 'Store Buildings (Taxpayers Included) - Predominant Retail with Other Uses', - K5: 'Store Buildings (Taxpayers Included) - Stand Alone Food Establishment', - K6: 'Store Buildings (Taxpayers Included) - Shopping Centers With or Without Parking', - K7: 'Store Buildings (Taxpayers Included) - Banking Facilities with or Without Parking', - K8: 'Store Buildings (Taxpayers Included) - Big Box Retail Not Affixed & Standing On Own Lot with Parking', - K9: 'Store Buildings (Taxpayers Included) - Miscellaneous', - - L1: 'Loft Buildinghs - Over Eight Stores (Mid-Manhattan Type)', - L2: 'Loft Buildinghs - Fireproof and Storage Type (Without Stores)', - L3: 'Loft Buildinghs - Semi-Fireproof', - L8: 'Loft Buildinghs - With Retail Stores Other Than Type 1', - L9: 'Loft Buildinghs - Miscellaneous', - - M1: 'Churches, Synagogues, etc. - Church, Synagogue, Chapel', - M2: 'Churches, Synagogues, etc. - Mission House (Non-Residential)', - M3: 'Churches, Synagogues, etc. - Parsonage, Rectory', - M4: 'Churches, Synagogues, etc. - Convents', - M9: 'Churches, Synagogues, etc. - Miscellaneous', - - N1: 'Asylums and Homes - Asylums', - N2: 'Asylums and Homes - Homes for Indigent Children, Aged, Homeless', - N3: 'Asylums and Homes - Orphanages', - N4: 'Asylums and Homes - Detention House For Wayward Girls', - N9: 'Asylums and Homes - Miscellaneous', - - O1: 'Office Buildings - Office Only – 1 Story', - O2: 'Office Buildings - Office Only – 2-6 Stories', - O3: 'Office Buildings - Office Only – 7-19 Stories', - O4: 'Office Buildings - Office Only or Office with Comm – 20 Stories or More', - O5: 'Office Buildings - Office with Comm – 1 to 6 Stories', - O6: 'Office Buildings - Office with Comm – 7 to 19 Stories', - O7: 'Office Buildings - Professional Buildings/Stand Alone Funeral Homes', - O8: 'Office Buildings - Office with Apartments Only (No Comm)', - O9: 'Office Buildings - Miscellaneous and Old Style Bank Bldgs', - - P1: 'Places of Public Assembly (indoor) and Cultural - Concert Halls', - P2: 'Places of Public Assembly (indoor) and Cultural - Lodge Rooms', - P3: 'Places of Public Assembly (indoor) and Cultural - YWCA, YMCA, YWHA, YMHA, PAL', - P4: 'Places of Public Assembly (indoor) and Cultural - Beach Club', - P5: 'Places of Public Assembly (indoor) and Cultural - Community Center', - P6: 'Places of Public Assembly (indoor) and Cultural - Amusement Place, Bathhouse, Boat House', - P7: 'Places of Public Assembly (indoor) and Cultural - Museum', - P8: 'Places of Public Assembly (indoor) and Cultural - Library', - P9: 'Places of Public Assembly (indoor) and Cultural - Miscellaneous', - - Q0: 'Outdoor Recreation Facilities - Open Space', - Q1: 'Outdoor Recreation Facilities - Parks/Recreation Facilities', - Q2: 'Outdoor Recreation Facilities - Playground', - Q3: 'Outdoor Recreation Facilities - Outdoor Pool', - Q4: 'Outdoor Recreation Facilities - Beach', - Q5: 'Outdoor Recreation Facilities - Golf Course', - Q6: 'Outdoor Recreation Facilities - Stadium, Race Track, Baseball Field', - Q7: 'Outdoor Recreation Facilities - Tennis Court', - Q8: 'Outdoor Recreation Facilities - Marina, Yacht Club', - Q9: 'Outdoor Recreation Facilities - Miscellaneous', - - R0: 'Condominiums - Condo Billing Lot', - R1: 'Condominiums - Residential Unit in 2-10 Unit Bldg', - R2: 'Condominiums - Residential Unit in Walk-Up Bldg', - R3: 'Condominiums - Residential Unit in 1-3 Story Bldg', - R4: 'Condominiums - Residential Unit in Elevator Bldg', - R5: 'Condominiums - Miscellaneous Commercial', - R6: 'Condominiums - Residential Unit of 1-3 Unit Bldg-Orig Class 1', - R7: 'Condominiums - Commercial Unit of 1-3 Units Bldg- Orig Class 1', - R8: 'Condominiums - Commercial Unit of 2-10 Unit Bldg', - R9: 'Condominiums - Co-op within a Condominium', - RA: 'Condominiums - Cultural, Medical, Educational, etc.', - RB: 'Condominiums - Office Space', - RC: 'Condominiums - Commercial Building (Mixed Commercial Condo Building Classification Codes)', - RD: 'Condominiums - Residential Building (Mixed Residential Condo Building Classification Codes)', - RG: 'Condominiums - Indoor Parking', - RH: 'Condominiums - Hotel/Boatel', - RI: 'Condominiums - Mixed Warehouse/Factory/Industrial & Commercial', - RK: 'Condominiums - Retail Space', - RM: 'Condominiums - Mixed Residential & Commercial Building (Mixed Residential & Commercial)', - RP: 'Condominiums - Outdoor Parking', - RR: 'Condominiums - Condominium Rentals', - RS: 'Condominiums - Non-Business Storage Space', - RT: 'Condominiums - Terraces/Gardens/Cabanas', - RW: 'Condominiums - Warehouse/Factory/Industrial', - RX: 'Condominiums - Mixed Residential, Commercial & Industrial', - RZ: 'Condominiums - Mixed Residential & Warehouse', - - S0: 'Residence (Multiple Use) - Primarily One Family with Two Stores or Offices', - S1: 'Residence (Multiple Use) - Primarily One Family with One Store or Office', - S2: 'Residence (Multiple Use) - Primarily Two Family with One Store or Office', - S3: 'Residence (Multiple Use) - Primarily Three Family with One Store or Office', - S4: 'Residence (Multiple Use) - Primarily Four Family with One Store or Office', - S5: 'Residence (Multiple Use) - Primarily Five to Six Family with One Store or Office', - S9: 'Residence (Multiple Use) - Single or Multiple Dwelling with Stores or Offices', - - T1: 'Transportation Facilities (Assessed in ORE) - Airport, Air Field, Terminal', - T2: 'Transportation Facilities (Assessed in ORE) - Pier, Dock, Bulkhead', - T9: 'Transportation Facilities (Assessed in ORE) - Miscellaneous', - - U0: 'Utility Bureau Properties - Utility Company Land and Building', - U1: 'Utility Bureau Properties - Bridge, Tunnel, Highway', - U2: 'Utility Bureau Properties - Gas or Electric Utility', - U3: 'Utility Bureau Properties - Ceiling Railroad', - U4: 'Utility Bureau Properties - Telephone Utility', - U5: 'Utility Bureau Properties - Communications Facilities Other Than Telephone', - U6: 'Utility Bureau Properties - Railroad - Private Ownership', - U7: 'Utility Bureau Properties - Transportation - Public Ownership', - U8: 'Utility Bureau Properties - Revocable Consent', - U9: 'Utility Bureau Properties - Miscellaneous', - - V0: 'Vacant Land - Zoned Residential; Not Manhattan', - V1: 'Vacant Land - Zoned Commercial or Manhattan Residential', - V2: 'Vacant Land - Zoned Commercial Adjacent to Class 1 Dwelling; Not Manhattan', - V3: 'Vacant Land - Zoned Primarily Residential; Not Manhattan', - V4: 'Vacant Land - Police or Fire Department', - V5: 'Vacant Land - School Site or Yard', - V6: 'Vacant Land - Library, Hospital or Museum', - V7: 'Vacant Land - Port Authority of NY and NJ', - V8: 'Vacant Land - New York State & U.S. Government', - V9: 'Vacant Land - Miscellaneous', - - W1: 'Educational Structures - Public Elementary, Junior or Senior High', - W2: 'Educational Structures - Parochial School, Yeshiva', - W3: 'Educational Structures - School or Academy', - W4: 'Educational Structures - Training School', - W5: 'Educational Structures - City University', - W6: 'Educational Structures - Other College and University', - W7: 'Educational Structures - Theological Seminary', - W8: 'Educational Structures - Other Private School', - W9: 'Educational Structures - Miscellaneous', - - Y1: 'Selected Government Installations (Excluding Office Buildings, Training Schools, Academic, Garages, Warehouses, Piers, Air Fields, Vacant Land, Vacant Sites, and Land Under Water and Easements) - Fire Department', - Y2: 'Selected Government Installations (Excluding Office Buildings, Training Schools, Academic, Garages, Warehouses, Piers, Air Fields, Vacant Land, Vacant Sites, and Land Under Water and Easements) - Police Department', - Y3: 'Selected Government Installations (Excluding Office Buildings, Training Schools, Academic, Garages, Warehouses, Piers, Air Fields, Vacant Land, Vacant Sites, and Land Under Water and Easements) - Prison, Jail, House of Detention', - Y4: 'Selected Government Installations (Excluding Office Buildings, Training Schools, Academic, Garages, Warehouses, Piers, Air Fields, Vacant Land, Vacant Sites, and Land Under Water and Easements) - Military and Naval Installation', - Y5: 'Selected Government Installations (Excluding Office Buildings, Training Schools, Academic, Garages, Warehouses, Piers, Air Fields, Vacant Land, Vacant Sites, and Land Under Water and Easements) - Department of Real Estate', - Y6: 'Selected Government Installations (Excluding Office Buildings, Training Schools, Academic, Garages, Warehouses, Piers, Air Fields, Vacant Land, Vacant Sites, and Land Under Water and Easements) - Department of Sanitation', - Y7: 'Selected Government Installations (Excluding Office Buildings, Training Schools, Academic, Garages, Warehouses, Piers, Air Fields, Vacant Land, Vacant Sites, and Land Under Water and Easements) - Department of Ports and Terminals', - Y8: 'Selected Government Installations (Excluding Office Buildings, Training Schools, Academic, Garages, Warehouses, Piers, Air Fields, Vacant Land, Vacant Sites, and Land Under Water and Easements) - Department of Public Works', - Y9: 'Selected Government Installations (Excluding Office Buildings, Training Schools, Academic, Garages, Warehouses, Piers, Air Fields, Vacant Land, Vacant Sites, and Land Under Water and Easements) - Department of Environmental Protection', - - Z0: 'Miscellaneous - Tennis Court, Pool, Shed, etc.', - Z1: 'Miscellaneous - Court House', - Z2: 'Miscellaneous - Public Parking Area', - Z3: 'Miscellaneous - Post Office', - Z4: 'Miscellaneous - Foreign Government', - Z5: 'Miscellaneous - United Nations', - Z7: 'Miscellaneous - Easement', - Z8: 'Miscellaneous - Cemetery', - Z9: 'Miscellaneous - Other', -}; - -const boroughLookup = { - BX: 'Bronx', - BK: 'Brooklyn', - MN: 'Manhattan', - QN: 'Queens', - SI: 'Staten Island', -}; - -const boroLookup = { - 1: 'Manhattan', - 2: 'Bronx', - 3: 'Brooklyn', - 4: 'Queens', - 5: 'Staten Island', -}; - -const ownertypeLookup = { - C: 'City', - M: 'Mixed City & Private', - O: 'Public Authority, State, or Federal', - P: 'Private', - X: 'Mixed', -}; - -const landuseLookup = { - '01': 'One & Two Family Buildings', - '02': 'Multi-Family Walk-Up Buildings', - '03': 'Multi-Family Elevator Buildings', - '04': 'Mixed Residential & Commercial Buildings', - '05': 'Commercial & Office Buildings', - '06': 'Industrial & Manufacturing', - '07': 'Transportation & Utility', - '08': 'Public Facilities & Institutions', - '09': 'Open Space & Outdoor Recreation', - 10: 'Parking Facilities', - 11: 'Vacant Land', -}; - -// this model fragment structures the "properties" -// node of a geojson feature -export default class LotFragment extends MF.Fragment { - @attr('string') address; - - @attr('number') bbl; - - @attr('number') bldgarea; - - @attr('string') bldgclass; - - @attr('string') borocode; - - @attr('number') lat; - - @attr('number') lon; - - @attr('number') block; - - @attr('string') borough; - - @attr('string') cd; - - @attr('number') condono; - - @attr('string') council; - - @attr('string') firecomp; - - @attr('string') histdist; - - @attr('string') landmark; - - @attr('string') landuse; - - @attr('number') lot; - - @attr('number') lotarea; - - @attr('number') lotdepth; - - @attr('number') lotfront; - - @attr('number') notes; - - @attr('number') numbldgs; - - @attr('number') numfloors; - - @attr('string') ownername; - - @attr('string') ownertype; - - @attr('string') overlay1; - - @attr('string') overlay2; - - @attr('string') policeprct; - - @attr('string') sanitboro; - - @attr('string') sanitdistr; - - @attr('string') sanitsub; - - @attr('string') schooldist; - - @attr('string') spdist1; - - @attr('string') spdist2; - - @attr('string') spdist3; - - @attr('number') unitsres; - - @attr('number') unitstotal; - - @attr('string') yearbuilt; - - @attr('number') yearalter1; - - @attr('number') yearalter2; - - @attr('number') zipcode; - - @attr('string') zonedist1; - - @attr('string') zonedist2; - - @attr('string') zonedist3; - - @attr('string') zonedist4; - - @attr('string') zonemap; - - @alias('borocode') boro; - - @computed('bldgclass') - get bldgclassname() { - return bldgclassLookup[this.bldgclass]; - } - - @computed('borough') - get boroname() { - return boroughLookup[this.borough]; - } - - @computed('cd') - get cdName() { - const borocd = this.cd; - const cdborocode = `${borocd}`.substring(0, 1); - const cd = parseInt(`${borocd}`.substring(1, 3), 10).toString(); - return `${boroLookup[cdborocode]} Community District ${cd}`; - } - - @computed('cd') - get cdURLSegment() { - const borocd = this.cd; - const borocode = this.borocode; // eslint-disable-line prefer-destructuring - const cleanBorough = boroLookup[borocode].toLowerCase().replace(/\s/g, '-'); - const cd = parseInt(`${borocd}`.substring(1, 3), 10).toString(); - return `${cleanBorough}/${cd}`; - } - - @computed('landuse') - get landusename() { - return landuseLookup[this.landuse]; - } - - @computed('ownertype') - get ownertypename() { - return ownertypeLookup[this.ownertype]; - } - - @computed('address') - get housenum() { - const match = this.address.match(/([0-9-]*)\s[0-9A-Za-z\s]*/); - return match ? match[1] : ''; - } - - @computed('address') - get street() { - const match = this.address.match(/[0-9-]*\s([0-9A-Za-z\s]*)/); - return match ? match[1] : ''; - } - - @computed('zonemap') - get paddedZonemap() { - const zonemap = this.get('zonemap'); - return (`0${zonemap}`).slice(-3); - } - - @computed('zonedist1') - get primaryzone1() { - const zonedist = this.get('zonedist1'); - return getPrimaryZone(zonedist); - } - - @computed('zonedist2') - get primaryzone2() { - const zonedist = this.get('zonedist2'); - return getPrimaryZone(zonedist); - } - - @computed('zonedist3') - get primaryzone3() { - const zonedist = this.get('zonedist3'); - return getPrimaryZone(zonedist); - } - - @computed('zonedist4') - get primaryzone4() { - const zonedist = this.get('zonedist4'); - return getPrimaryZone(zonedist); - } - - @computed('spdist1', 'spdist2', 'spdist3') - get parentSpecialPurposeDistricts() { - const DISTRICT_TOOLS_URL = 'https://www1.nyc.gov/site/planning/zoning/districts-tools'; - const spdist1 = this.get('spdist1'); - const spdist2 = this.get('spdist2'); - const spdist3 = this.get('spdist3'); - - return carto.SQL(specialPurposeDistrictsSQL('dcp_special_purpose_districts', spdist1, spdist2, spdist3)) - .then(response => response.map( - (item) => { - const [, [anchorName, boroName]] = specialDistrictCrosswalk - .find(([dist]) => dist === item.sdname); - const specialDistrictLink = `${DISTRICT_TOOLS_URL}/special-purpose-districts-${boroName}.page#${anchorName}`; - - return { - label: item.sdlbl.toUpperCase(), - name: item.sdname, - anchorName, - boroName, - specialDistrictLink, - }; - }, - )); - } - - @computed('borocode', 'block', 'lot') - get biswebLink() { - const BISWEB_HOST = 'http://a810-bisweb.nyc.gov/bisweb/PropertyBrowseByBBLServlet'; - - return `${BISWEB_HOST}?allborough=${this.borocode}&allblock=${this.block}&alllot=${this.lot}&go5=+GO+&requestid=0`; - } - - @computed('cdURLSegment') - get fullCommunityDistrictURL() { - return `https://communityprofiles.planning.nyc.gov/${this.cdURLSegment}`; - } - - @computed('primaryzone1', 'primaryzone2', 'primaryzone3', 'primaryzone4') - get zoneDistLinks() { - const primaryZones = this.getProperties('primaryzone1', 'primaryzone2', 'primaryzone3', 'primaryzone4'); - - Object.keys(primaryZones).forEach((key) => { - const value = primaryZones[key]; - primaryZones[key] = `https://www1.nyc.gov/site/planning/zoning/districts-tools/${value}.page`; - }); - - return { - ...primaryZones, - }; - } - - @computed('bbl') - get digitalTaxMapLink() { - return `http://maps.nyc.gov/taxmap/map.htm?searchType=BblSearch&featureTypeName=EVERY_BBL&featureName=${this.bbl}`; - } - - @computed('zonemap') - get zoningMapLink() { - return `https://s-media.nyc.gov/agencies/dcp/assets/files/pdf/zoning/zoning-maps/map${this.zonemap}.pdf`; - } - - @computed('paddedZonemap') - get historicalZoningMapLink() { - return `https://s-media.nyc.gov/agencies/dcp/assets/files/pdf/zoning/zoning-maps/maps${this.paddedZonemap}.pdf`; - } - - @computed('borocode', 'block', 'lot') - get ACRISLink() { - return `http://a836-acris.nyc.gov/bblsearch/bblsearch.asp?borough=${this.borocode}&block=${this.block}&lot=${this.lot}`; - } - - @computed('council') - get councilLink() { - return `https://council.nyc.gov/district-${this.council}/`; - } -} diff --git a/app/models/map-features/special-purpose-district.js b/app/models/map-features/special-purpose-district.js deleted file mode 100644 index 5fcbaed84..000000000 --- a/app/models/map-features/special-purpose-district.js +++ /dev/null @@ -1,25 +0,0 @@ -import DS from 'ember-data'; -import MF from 'ember-data-model-fragments'; -import config from 'labs-zola/config/environment'; -import { computed } from '@ember/object'; - -const { specialDistrictCrosswalk } = config; -const { attr } = DS; - -// this model fragment structures the "properties" -// node of a geojson feature -export default class SpecialPurposeDistrictFragment extends MF.Fragment { - @attr('string') - sdlbl; - - @attr('string') - sdname; - - @computed('sdname') - get readMoreLink() { - const name = this.get('sdname'); - const [, [anchorName, boroName]] = specialDistrictCrosswalk - .find(([dist]) => dist === name) || [[], []]; - return `https://www1.nyc.gov/site/planning/zoning/districts-tools/special-purpose-districts-${boroName}.page#${anchorName}`; - } -} diff --git a/app/models/map-features/special-purpose-subdistrict.js b/app/models/map-features/special-purpose-subdistrict.js deleted file mode 100644 index 333c9b976..000000000 --- a/app/models/map-features/special-purpose-subdistrict.js +++ /dev/null @@ -1,27 +0,0 @@ -import DS from 'ember-data'; -import MF from 'ember-data-model-fragments'; -import { computed } from '@ember/object'; -import config from 'labs-zola/config/environment'; - -const { specialDistrictCrosswalk } = config; - -const { attr } = DS; - -// this model fragment structures the "properties" -// node of a geojson feature -export default class SpecialPurposeSubdistrictFragment extends MF.Fragment { - @attr('string') - splbl; - - @attr('string') - spname; - - @computed('sdname') - get readMoreLink() { - const name = this.get('sdname'); - const [, [anchorName, boroName]] = specialDistrictCrosswalk - .find(([dist]) => dist === name) || [[], []]; - - return `https://www1.nyc.gov/site/planning/zoning/districts-tools/special-purpose-districts-${boroName}.page#${anchorName}`; - } -} diff --git a/app/models/map-features/zoning-district.js b/app/models/map-features/zoning-district.js deleted file mode 100644 index b351020dd..000000000 --- a/app/models/map-features/zoning-district.js +++ /dev/null @@ -1,136 +0,0 @@ -import DS from 'ember-data'; -import MF from 'ember-data-model-fragments'; -import { computed } from '@ember/object'; - -const zoningDescriptions = { - m1: 'M1 districts are designated for areas with light industries.', - m2: 'M2 districts occupy the middle ground between light and heavy industrial areas.', - m3: 'M3 districts are designated for areas with heavy industries that generate noise, traffic or pollutants.', - c1: 'C1 districts are mapped along streets that serve local retail needs within residential neighborhoods.', - c2: 'C2 districts are mapped along streets that serve local retail needs within residential neighborhoods.', - c3: 'C3 districts permit waterfront recreational activities, primarily boating and fishing, in areas along the waterfront.', - c4: 'C4 districts are mapped in regional centers where larger stores, theaters and office uses serve a wider region and generate more traffic than neighborhood shopping areas.', - c5: 'C5 districts are intended for commercial areas that require central locations or serve the entire metropolitan region.', - c6: 'C6 districts are intended for commercial areas that require central locations or serve the entire metropolitan region.', - c7: 'C7 districts are specifically designated for large open amusement parks.', - c8: 'C8 districts, bridging commercial and manufacturing uses, provide for automotive and other heavy commercial services that often require large amounts of land.', - p: 'A public park is any park, playground, beach, parkway, or roadway within the jurisdiction and control of the New York City Commissioner of Parks & Recreation. Typically, public parks are not subject to zoning regulations.', - r1: 'R1 districts are leafy, low-density neighborhoods of large, single-family detached homes on spacious lots.', - r2: 'Residential development in R2 districts is limited exclusively to single-family detached houses.', - r2a: 'R2A is a contextual district intended to preserve low-rise neighborhoods characterized by single-family detached homes on lots with a minimum width of 40 feet', - r2x: 'R2X districts allow large single-family detached houses on lots with a minimum width of 30 feet.', - r31: 'R3-1 contextual districts are the lowest density districts that allow semi-detached one- and two-family residences, as well as detached homes', - r32: 'R3-2 districts are general residence districts that allow a variety of housing types, including low-rise attached houses, small multifamily apartment houses, and detached and semi-detached one- and two-family residences.', - r3a: 'Characteristic of many of the city’s older neighborhoods, R3A contextual districts feature modest single- and two-family detached residences on zoning lots as narrow as 25 feet in width.', - r3x: 'R3X contextual districts, mapped extensively in lower-density neighborhoods permit only one- and two-family detached homes on lots that must be at least 35 feet wide.', - r4: 'R4 districts are general residence districts that allow a variety of housing types, including low-rise attached houses, small multifamily apartment houses, and detached and semi-detached one- and two-family residences.', - r41: 'R4-1 contextual districts permit only one- and two-family detached and semi-detached houses.', - r4a: 'R4A contextual districts permit only one- and two-family detached residences characterized by houses with two stories and an attic beneath a pitched roof.', - r4b: 'Primarily a contextual rowhouse district limited to low-rise, one- and two-family attached residences, R4B districts also permit detached and semi-detached buildings.', - r5: 'R5 districts are general residence districts that allow a variety of housing types, including low-rise attached houses, small multifamily apartment houses, and detached and semi-detached one- and two-family residences.', - r5a: 'R5A contextual districts permit only one- and two-family detached residences characterized by houses with two stories and an attic beneath a pitched roof.', - r5b: 'Primarily a contextual rowhouse district limited to low-rise, one- and two-family attached residences, R4B districts also permit detached and semi-detached buildings.', - r5d: 'R5D contextual districts are designed to encourage residential growth along major corridors in auto-dependent areas of the city.', - r6: 'R6 zoning districts are widely mapped in built-up, medium-density areas of the city whose character can range from neighborhoods with a diverse mix of building types and heights to large-scale “tower in the park” developments.', - r6a: 'R6A contextual districts produce high lot coverage, six- to eight-story apartment buildings set at or near the street line designed to be compatible with older buildings in medium-density neighborhoods.', - r6b: 'R6B contextual districts are often traditional row house districts, which preserve the scale and harmonious streetscape of medium-density neighborhoods of four-story attached buildings developed during the 19th century.', - r7: 'R7 zoning districts are medium-density apartment house districts that encourage lower apartment buildings on smaller lots and, on larger lots, taller buildings with less lot coverage.', - r7a: 'R7A contextual districts produce high lot coverage, seven- to nine-story apartment buildings set at or near the street line designed to be compatible with older buildings in medium-density neighborhoods.', - r7b: 'R7B contextual districts generally produce six- to seven-story apartment buildings in medium-density neighborhoods.', - r7d: 'R7D contextual districts promote new medium-density contextual development along transit corridors that range between 10 and 11 stories.', - r7x: 'R7X contextual districts are flexible medium-density districts that generally produce 12- to 14-story buildings.', - r8: 'R8 zoning districts are high-density apartment house districts that encourage mid-rise apartment buildings on smaller lots and, on larger lots, taller buildings with less lot coverage.', - r8a: 'R8A contextual districts are high-density districts designed to produce apartment buildings at heights of roughly twelve to fourteen stories.', - r8b: 'R8B contextual districts are designed to preserve the character and scale of taller rowhouse neighborhoods.', - r8x: 'R8X contextual districts are flexible high-density districts that generally produce 15- to 17-story buildings.', - r9: 'R9 districts are high-density districts that permit a wide range of building types including towers.', - r9a: 'R9A contextual districts are high-density districts designed to produce new buildings between 13 and 17 stories that mimics older, high street wall buildings in high-density neighborhoods.', - r9d: 'R9D contextual districts are high-density districts that permit towers that sit on a contextual base.', - r9x: 'R9X contextual districts are high-density districts designed to produce new buildings between 16 and 20 stories that mimics older, high street wall buildings in high-density neighborhoods.', - r10: 'R10 districts are high-density districts that permit a wide range of building types including towers.', - r10a: 'R10-A contextual districts are high-density districts designed to produce new buildings between 21 and 23 stories that mimics older, high street wall buildings in high-density neighborhoods.', - r10x: 'R10X contextual districts are high-density districts that permit towers that sit on a contextual base.', - bpc: 'The Special Battery Park City District (BPC) was created, in accordance with a master plan, to govern extensive residential and commercial development in an area on the Hudson River close to the business core of Lower Manhattan. The district regulates permitted uses and bulk within three specified areas and establishes special design controls with respect to front building walls, building heights, waterfront design and parking.', -}; - -const zoningAbbr = { - R2A: 'r2a', - R2X: 'r2x', - 'R3-1': 'r31', - 'R3-2': 'r32', - R3A: 'r3a', - R3X: 'r3x', - 'R4-1': 'r41', - R4A: 'r4a', - R4B: 'r4b', - R5A: 'r5a', - R5B: 'r5b', - R5D: 'r5d', - R6A: 'r6a', - R6B: 'r6b', - R7A: 'r7a', - R7B: 'r7b', - R7D: 'r7d', - R7X: 'r7x', - R8A: 'r8a', - R8B: 'r8b', - R8X: 'r8x', - R9A: 'r9a', - R9D: 'r9d', // R9D does not have a route - R9X: 'r9x', - R10A: 'r10a', - R10X: 'r10x', // R10X does not have a route - BPC: 'bpc', -}; - -const { attr } = DS; - -// this model fragment structures the "properties" -// node of a geojson feature -export default class ZoningDistrictFragment extends MF.Fragment { - @attr('string') - zonedist; - - @computed('zonedist') - get primaryzone() { - const zonedist = this.get('zonedist'); - // convert R6A to r6 - const primary = zonedist.match(/\w\d*/)[0].toLowerCase(); - return primary; - } - - @computed('zonedist') - get zoneabbr() { - const zonedist = this.get('zonedist'); - const abbr = zonedist.match(/\w\d*/)[0].toLowerCase(); - - if (zonedist in zoningAbbr) { - return zoningAbbr[zonedist]; - } - - return abbr; - } - - @computed('zoneabbr') - get description() { - const zoneabbr = this.get('zoneabbr'); - - return zoningDescriptions[zoneabbr]; - } - - @computed('primaryzone') - get primaryzoneURL() { - const primaryzone = this.get('primaryzone'); - let url = ''; - - if ((primaryzone === 'c1') || (primaryzone === 'c2')) { - url = 'c1-c2'; - } else if (primaryzone === 'c3') { - url = 'c3-c3a'; - } else { - url = primaryzone; - } - - return url; - } -} diff --git a/app/models/map-features/zoning-map-amendment.js b/app/models/map-features/zoning-map-amendment.js deleted file mode 100644 index 8943c6af3..000000000 --- a/app/models/map-features/zoning-map-amendment.js +++ /dev/null @@ -1,31 +0,0 @@ -import DS from 'ember-data'; -import MF from 'ember-data-model-fragments'; -import { computed } from '@ember/object'; - -const { attr } = DS; - -// this model fragment structures the "properties" -// node of a geojson feature -export default class ZoningMapAmendmentFragment extends MF.Fragment { - @attr('string') ulurpno; - - @attr('string') project_na; - - @attr('string') effective; - - @attr('string') status; - - @attr('string') lucats; - - @computed('effective') - get effectiveDisplay() { - return import('moment').then(({ default: moment }) => { - const effective = this.get('effective'); - - if (effective) { - return moment(effective).utc().format('LL'); - } - return 'To be determined'; - }); - } -} diff --git a/app/models/source.js b/app/models/source.js index 9c9821123..774882704 100644 --- a/app/models/source.js +++ b/app/models/source.js @@ -1,6 +1,6 @@ -import DS from 'ember-data'; +import Model, { attr } from '@ember-data/model'; -export default DS.Model.extend({ - meta: DS.attr(), - minzoom: DS.attr('number'), +export default Model.extend({ + meta: attr(), + minzoom: attr('number'), }); diff --git a/app/models/special-purpose-district.js b/app/models/special-purpose-district.js index d57d6a50b..d8b6c4294 100644 --- a/app/models/special-purpose-district.js +++ b/app/models/special-purpose-district.js @@ -1,16 +1,11 @@ -import { - fragment, -} from 'ember-data-model-fragments/attributes'; +import { attr } from '@ember-data/model'; import { alias } from '@ember/object/computed'; import CartoGeojsonFeature from './carto-geojson-feature'; export default class SpecialPurposeDistrict extends CartoGeojsonFeature { - @fragment('map-features/special-purpose-district') - properties; + @attr properties; - @alias('properties.sdname') - title; + @alias('properties.sdname') title; - @alias('properties.sdlbl') - subtitle; + @alias('properties.sdlbl') subtitle; } diff --git a/app/models/special-purpose-subdistrict.js b/app/models/special-purpose-subdistrict.js index 2b41baf2e..12adf9299 100644 --- a/app/models/special-purpose-subdistrict.js +++ b/app/models/special-purpose-subdistrict.js @@ -1,16 +1,11 @@ -import { - fragment, -} from 'ember-data-model-fragments/attributes'; +import { attr } from '@ember-data/model'; import { alias } from '@ember/object/computed'; import CartoGeojsonFeature from './carto-geojson-feature'; export default class SpecialPurposeSubdistrict extends CartoGeojsonFeature { - @fragment('map-features/special-purpose-subdistrict') - properties; + @attr properties; - @alias('properties.spname') - title; + @alias('properties.spname') title; - @alias('properties.splbl') - subtitle; + @alias('properties.splbl') subtitle; } diff --git a/app/models/zoning-district.js b/app/models/zoning-district.js index 8d471a037..30b37a20d 100644 --- a/app/models/zoning-district.js +++ b/app/models/zoning-district.js @@ -1,13 +1,9 @@ -import { - fragment, -} from 'ember-data-model-fragments/attributes'; +import { attr } from '@ember-data/model'; import { alias } from '@ember/object/computed'; import CartoGeojsonFeature from './carto-geojson-feature'; export default class ZoningDistrict extends CartoGeojsonFeature { - @fragment('map-features/zoning-district') - properties; + @attr properties; - @alias('properties.zonedist') - title; + @alias('properties.zonedist') title; } diff --git a/app/models/zoning-map-amendment.js b/app/models/zoning-map-amendment.js index 58c8bb2ee..dcea177ae 100644 --- a/app/models/zoning-map-amendment.js +++ b/app/models/zoning-map-amendment.js @@ -1,16 +1,11 @@ -import { - fragment, -} from 'ember-data-model-fragments/attributes'; +import { attr } from '@ember-data/model'; import { alias } from '@ember/object/computed'; import CartoGeojsonFeature from './carto-geojson-feature'; export default class ZoningMapAmendment extends CartoGeojsonFeature { - @fragment('map-features/zoning-map-amendment') - properties; + @attr properties; - @alias('properties.project_na') - title; + @alias('properties.project_na') title; - @alias('properties.lucats') - subtitle; + @alias('properties.lucats') subtitle; } diff --git a/app/router.js b/app/router.js index bc9320c71..070a1ce16 100644 --- a/app/router.js +++ b/app/router.js @@ -1,9 +1,10 @@ import EmberRouter from '@ember/routing/router'; -import trackPage from './mixins/track-page'; import config from 'labs-zola/config/environment'; +import trackPage from './mixins/track-page'; export default class Router extends EmberRouter.extend(trackPage) { location = config.locationType; + rootURL = config.rootURL; } @@ -19,12 +20,16 @@ Router.map(function () {// eslint-disable-line this.route('features'); // views for individual records of data - this.route('map-feature', { path: '/l' }, function() { + this.route('map-feature', { path: '/l' }, function () { this.route('lot', { path: 'lot/:boro/:block/:lot' }); this.route('zoning-district', { path: '/zoning-district/:id' }); this.route('commercial-overlay', { path: '/commercial-overlay/:id' }); - this.route('special-purpose-district', { path: '/special-purpose-district/:id' }); - this.route('special-purpose-subdistrict', { path: '/special-purpose-subdistrict/:id' }); + this.route('special-purpose-district', { + path: '/special-purpose-district/:id', + }); + this.route('special-purpose-subdistrict', { + path: '/special-purpose-subdistrict/:id', + }); this.route('zoning-map-amendment', { path: '/zma/:id' }); this.route('e-designation', { path: '/e-designation/:id' }); }); diff --git a/app/routes/about.js b/app/routes/about.js index 6c74252aa..d09f667b2 100644 --- a/app/routes/about.js +++ b/app/routes/about.js @@ -1,4 +1,3 @@ import Route from '@ember/routing/route'; -export default Route.extend({ -}); +export default Route.extend({}); diff --git a/app/routes/application.js b/app/routes/application.js index 88d0b10a7..1abb8f6d0 100644 --- a/app/routes/application.js +++ b/app/routes/application.js @@ -8,6 +8,7 @@ export default Route.extend({ mainMap: service(), fastboot: service(), router: service(), + store: service(), beforeModel(transition) { const { targetName } = transition; @@ -33,9 +34,7 @@ export default Route.extend({ }, async model() { - const { - layerGroups: layerGroupsParams, - } = this.paramsFor('application'); + const { layerGroups: layerGroupsParams } = this.paramsFor('application'); // fetch layer groups based on configured environment variable const layerGroups = await this.store.query('layer-group', { @@ -51,13 +50,10 @@ export default Route.extend({ const { meta } = layerGroups; // pass down a hash representation of the layer group ids - const layerGroupsObject = layerGroups.reduce( - (accumulator, current) => { - accumulator[current.get('id')] = current; - return accumulator; - }, - {}, - ); + const layerGroupsObject = layerGroups.reduce((accumulator, current) => { + accumulator[current.get('id')] = current; + return accumulator; + }, {}); const bookmarks = await this.store.findAll('bookmark'); await bookmarks.invoke('get', 'bookmark'); diff --git a/app/routes/bbox.js b/app/routes/bbox.js index 3f458b8c3..09007096c 100644 --- a/app/routes/bbox.js +++ b/app/routes/bbox.js @@ -3,18 +3,13 @@ import { inject as service } from '@ember/service'; import bboxPolygon from '@turf/bbox-polygon'; import booleanWithin from '@turf/boolean-within'; -export const GREATER_NYC_BBOX = [-74.492798, 40.435450, -73.413391, 41.028607]; +export const GREATER_NYC_BBOX = [-74.492798, 40.43545, -73.413391, 41.028607]; export default Route.extend({ mainMap: service(), model(params) { - const { - west, - south, - east, - north, - } = params; + const { west, south, east, north } = params; if (!this.validateBounds([west, south, east, north])) { this.transitionTo('/'); @@ -24,7 +19,7 @@ export default Route.extend({ }, afterModel(bounds) { - this.get('mainMap.setBounds').perform(bounds); + this.mainMap.setBounds.perform(bounds); }, validateBounds(bounds) { diff --git a/app/routes/bookmarks.js b/app/routes/bookmarks.js index 9f4124d91..b60667aa3 100644 --- a/app/routes/bookmarks.js +++ b/app/routes/bookmarks.js @@ -3,6 +3,7 @@ import { inject as service } from '@ember/service'; export default Route.extend({ mainMap: service(), + store: service(), model() { return this.store.findAll('bookmark'); diff --git a/app/routes/data.js b/app/routes/data.js index a9bc805ea..3dc9fcd71 100644 --- a/app/routes/data.js +++ b/app/routes/data.js @@ -1,6 +1,8 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; export default Route.extend({ + store: service(), model() { const sources = this.store.peekAll('source'); return sources.toArray().uniqBy('meta.description'); diff --git a/app/routes/features.js b/app/routes/features.js index 6c74252aa..d09f667b2 100644 --- a/app/routes/features.js +++ b/app/routes/features.js @@ -1,4 +1,3 @@ import Route from '@ember/routing/route'; -export default Route.extend({ -}); +export default Route.extend({}); diff --git a/app/routes/legacy-redirects.js b/app/routes/legacy-redirects.js index 0acc92fe1..046156863 100644 --- a/app/routes/legacy-redirects.js +++ b/app/routes/legacy-redirects.js @@ -3,8 +3,7 @@ import { inject as service } from '@ember/service'; import window from 'ember-window-mock'; export default class LegacyRedirectsRoute extends Route { - @service - fastboot; + @service fastboot; beforeModel(transition) { if (!this.fastboot.isFastBoot) { diff --git a/app/routes/map-feature.js b/app/routes/map-feature.js index 5b6ef4f09..50082be1e 100644 --- a/app/routes/map-feature.js +++ b/app/routes/map-feature.js @@ -1,4 +1,3 @@ import Route from '@ember/routing/route'; -export default class MapFeature extends Route { -} +export default class MapFeature extends Route {} diff --git a/app/routes/map-feature/zoning-map-amendment.js b/app/routes/map-feature/zoning-map-amendment.js index bd84541fa..82406eb7b 100644 --- a/app/routes/map-feature/zoning-map-amendment.js +++ b/app/routes/map-feature/zoning-map-amendment.js @@ -1,5 +1,4 @@ import Route from '@ember/routing/route'; -import fetch from 'fetch'; import config from 'labs-zola/config/environment'; export default class ZoningDistrictRoute extends Route { @@ -8,9 +7,11 @@ export default class ZoningDistrictRoute extends Route { const { search } = this.paramsFor('map-feature'); try { - const response = await fetch(`${config.zapApiHost}/projects?action-ulurpnumber[]=${id}`); + const response = await fetch( + `${config.zapApiHost}/projects?action-ulurpnumber[]=${id}` + ); const ulurp = await response.json(); - const zapId = (ulurp.data.length === 1) ? ulurp.data[0].id : null; + const zapId = ulurp.data.length === 1 ? ulurp.data[0].id : null; return { id, diff --git a/app/serializers/carto-geojson-feature.js b/app/serializers/carto-geojson-feature.js index e22400b13..ed3a77d57 100644 --- a/app/serializers/carto-geojson-feature.js +++ b/app/serializers/carto-geojson-feature.js @@ -1,8 +1,14 @@ import { assign } from '@ember/polyfills'; -import DS from 'ember-data'; +import JSONSerializer from '@ember-data/serializer/json'; -export default class GeoJsonFeatureSerializer extends DS.JSONSerializer { - normalizeFindRecordResponse(store, primaryModelClass, payload, queryId, requestType) { +export default class GeoJsonFeatureSerializer extends JSONSerializer { + normalizeFindRecordResponse( + store, + primaryModelClass, + payload, + queryId, + requestType + ) { let newPayload = payload; let newQueryId = queryId; @@ -22,7 +28,7 @@ export default class GeoJsonFeatureSerializer extends DS.JSONSerializer { primaryModelClass, newPayload, newQueryId, - requestType, + requestType ); } @@ -39,7 +45,7 @@ export default class GeoJsonFeatureSerializer extends DS.JSONSerializer { store, primaryModelClass, features, - ...etc, + ...etc ); } } diff --git a/app/serializers/layer-group.js b/app/serializers/layer-group.js index 74689dcb5..6414656b0 100644 --- a/app/serializers/layer-group.js +++ b/app/serializers/layer-group.js @@ -1,4 +1,3 @@ -import DS from 'ember-data'; +import JSONAPISerializer from '@ember-data/serializer/json-api'; -export default DS.JSONAPISerializer.extend({ -}); +export default JSONAPISerializer.extend({}); diff --git a/app/serializers/layer.js b/app/serializers/layer.js index 74689dcb5..6414656b0 100644 --- a/app/serializers/layer.js +++ b/app/serializers/layer.js @@ -1,4 +1,3 @@ -import DS from 'ember-data'; +import JSONAPISerializer from '@ember-data/serializer/json-api'; -export default DS.JSONAPISerializer.extend({ -}); +export default JSONAPISerializer.extend({}); diff --git a/app/serializers/source.js b/app/serializers/source.js index 345228308..6414656b0 100644 --- a/app/serializers/source.js +++ b/app/serializers/source.js @@ -1,3 +1,3 @@ -import DS from 'ember-data'; +import JSONAPISerializer from '@ember-data/serializer/json-api'; -export default DS.JSONAPISerializer.extend({}); +export default JSONAPISerializer.extend({}); diff --git a/app/services/main-map.js b/app/services/main-map.js index 53e4635ab..09581f492 100644 --- a/app/services/main-map.js +++ b/app/services/main-map.js @@ -7,7 +7,10 @@ const DEFAULT_LNG = 40.7125; const DEFAULT_LAT = -73.733; const DEFAULT_LAT_OFFSET = -0.1692; const MIN_ZOOM = 9.5; -const MAX_BOUNDS = [[-74.5, 40.25], [-73, 41]]; +const MAX_BOUNDS = [ + [-74.5, 40.25], + [-73, 41], +]; export default class MainMapService extends Service { mapInstance = null; @@ -33,7 +36,7 @@ export default class MainMapService extends Service { knownHashIntent = ''; - @computed + @computed('knownHashIntent') get parsedHash() { if (this.knownHashIntent) { return this.knownHashIntent.replace('#', '').split('/').reverse(); @@ -42,7 +45,7 @@ export default class MainMapService extends Service { return [9.72, 40.7125, -73.733]; } - @computed + @computed('isSelectedBoundsOptions', 'knownHashIntent', 'parsedHash') get center() { if (this.knownHashIntent) { const [x, y] = this.parsedHash; @@ -50,32 +53,38 @@ export default class MainMapService extends Service { } const boundsOptions = this.isSelectedBoundsOptions; - const x = (boundsOptions.offset[0] === 0) ? (DEFAULT_LAT + DEFAULT_LAT_OFFSET) : DEFAULT_LAT; + const x = + boundsOptions.offset[0] === 0 + ? DEFAULT_LAT + DEFAULT_LAT_OFFSET + : DEFAULT_LAT; return [x, DEFAULT_LNG]; } @computed('selected', 'routeIntentIsNested') get isSelectedBoundsOptions() { - const selected = this.get('selected'); + const { selected } = this; const el = document.querySelector('.map-container'); const height = el.offsetHeight; const width = el.offsetWidth; - const routeIntentIsNested = this.get('routeIntentIsNested'); + const { routeIntentIsNested } = this; const fullWidth = window.innerWidth; // width of content area on large screens is 5/12 of full const contentWidth = (fullWidth / 12) * 5; // on small screens, no offset const offset = fullWidth < 1024 ? 0 : -((width - contentWidth) / 2) / 2; - const padding = Math.min(height, (width - contentWidth)) / 2.5; + const padding = Math.min(height, width - contentWidth) / 2.5; // get type of selected feature so we can do dynamic padding const type = selected ? selected.constructor.modelName : null; const options = { ...(routeIntentIsNested ? { duration: 0 } : {}), - padding: selected && (type !== 'zoning-district') && (type !== 'commercial-overlay') ? padding : 0, + padding: + selected && type !== 'zoning-district' && type !== 'commercial-overlay' + ? padding + : 0, offset: [offset, 0], }; @@ -83,15 +92,15 @@ export default class MainMapService extends Service { } @task(function* (explicitBounds) { - const bounds = explicitBounds || this.get('bounds'); - while (!this.get('mapInstance')) { + const bounds = explicitBounds || this.bounds; + while (!this.mapInstance) { yield timeout(100); } - const mapInstance = this.get('mapInstance'); + const { mapInstance } = this; - mapInstance.fitBounds(bounds, this.get('isSelectedBoundsOptions')); + mapInstance.fitBounds(bounds, this.isSelectedBoundsOptions); this.set('routeIntentIsNested', false); }) - setBounds + setBounds; } diff --git a/app/services/print.js b/app/services/print.js index 1f7d6a68a..44ad2fd81 100644 --- a/app/services/print.js +++ b/app/services/print.js @@ -4,8 +4,7 @@ import { computed } from '@ember/object'; export default class PrintService extends Service { enabled = false; - @service - metrics; + @service metrics; // Print View Settings printViewOrientation = 'portrait'; @@ -29,7 +28,12 @@ export default class PrintService extends Service { return hiddenAreasClasses.join(' '); } - @computed('printViewHiddenAreas', 'enabled', 'printViewPaperSize', 'printViewOrientation', 'printViewHiddenAreas') + @computed( + 'printViewHiddenAreas', + 'enabled', + 'printViewPaperSize', + 'printViewOrientation' + ) get printViewClasses() { const orientation = this.printViewOrientation; const size = this.printViewPaperSize; diff --git a/app/styles/app.css b/app/styles/app.css new file mode 100644 index 000000000..2763afa4c --- /dev/null +++ b/app/styles/app.css @@ -0,0 +1 @@ +/* Ember supports plain CSS out of the box. More info: https://cli.emberjs.com/release/advanced-use/stylesheets/ */ diff --git a/app/styles/app.scss b/app/styles/app.scss index 98e47b5cf..d0865094d 100644 --- a/app/styles/app.scss +++ b/app/styles/app.scss @@ -48,6 +48,5 @@ @import 'modules/_m-search'; @import 'ember-power-select'; -@import 'ember-content-placeholders'; @import 'layouts/_l-print'; diff --git a/app/styles/base/_typography.scss b/app/styles/base/_typography.scss index faa1ac126..c1e6d55d6 100644 --- a/app/styles/base/_typography.scss +++ b/app/styles/base/_typography.scss @@ -119,7 +119,7 @@ button.save-button { } .lot-zoning-list { - margin: $paragraph-margin-bottom/2 0; + margin: $paragraph-margin-bottom*0.5 0; list-style: none; li { @@ -161,7 +161,11 @@ button.save-button { } .data-label { - @include xy-cell(full,false,0); + @include xy-cell( + $size: full, + $output: (base size), + $gutters: 0 + ); padding: rem-calc(2) rem-calc(4); background-color: $off-white; color: $dark-gray; @@ -171,7 +175,7 @@ button.save-button { @include breakpoint(medium) { @include xy-cell( $size: 4, - $gutter-output: true, + $gutter-output: null, $gutters: rem-calc(10), $gutter-type: margin, $breakpoint: null, @@ -182,14 +186,17 @@ button.save-button { } .datum { - @include xy-cell(full,false,0); + @include xy-cell( + $size: full, + $output: (base size), + $gutters: 0 + ); font-size: rem-calc(13); line-height: 1.2; @include breakpoint(medium) { @include xy-cell( $size: 8, - $gutter-output: true, $gutters: rem-calc(10), $gutter-type: margin, $breakpoint: null, diff --git a/app/styles/layouts/_l-default.scss b/app/styles/layouts/_l-default.scss index 867c4a8fd..afddf4759 100644 --- a/app/styles/layouts/_l-default.scss +++ b/app/styles/layouts/_l-default.scss @@ -73,7 +73,11 @@ body { } .map-container { - @include xy-cell(full,false,0); + @include xy-cell( + $size: full, + $output: (base size), + $gutters: 0 + ); position: relative; z-index: 1; height: calc(50vh); @@ -120,7 +124,11 @@ body.index { // .search-and-layers { .layer-palette { - @include xy-cell(full,false,0); + @include xy-cell( + $size: full, + $output: (base size), + $gutters: 0 + ); position: relative; z-index: 2; background-color: $off-white; @@ -178,7 +186,11 @@ body.index { text-align: right; @include breakpoint(medium down) { - @include xy-cell(full,false,0); + @include xy-cell( + $size: full, + $output: (base size), + $gutters: 0 + ); } & > .close-button { diff --git a/app/styles/modules/_m-layer-palette.scss b/app/styles/modules/_m-layer-palette.scss index 2b8a7ea0c..5655eebef 100644 --- a/app/styles/modules/_m-layer-palette.scss +++ b/app/styles/modules/_m-layer-palette.scss @@ -19,7 +19,7 @@ $layer-palette-hover-color: rgba($dark-gray,0.08); .checkbox-wrapper { - padding: $layer-palette-padding/2 $layer-palette-padding; + padding: $layer-palette-padding*0.5 $layer-palette-padding; font-size: inherit; &:hover { @@ -51,7 +51,7 @@ $layer-palette-hover-color: rgba($dark-gray,0.08); } .nested { - margin-bottom: $layer-palette-padding/2; + margin-bottom: $layer-palette-padding*0.5; label { padding-left: $layer-palette-padding*3; @@ -114,7 +114,7 @@ $layer-palette-hover-color: rgba($dark-gray,0.08); line-height: 1.2; li { - padding: 0 0 $layer-palette-padding/2; + padding: 0 0 $layer-palette-padding*0.5; } .fa { diff --git a/app/styles/modules/_m-maps.scss b/app/styles/modules/_m-maps.scss index 77414948f..60c3ab9e1 100644 --- a/app/styles/modules/_m-maps.scss +++ b/app/styles/modules/_m-maps.scss @@ -16,18 +16,6 @@ z-index: 5; } -// Loading spinner -.loading-spinner { - color: $gray; - opacity: 0.5; - position: absolute; - z-index: 3; - bottom: 1.75rem; - right: 1rem; - margin: 0.25rem auto; - pointer-events: none; -} - // Geolocate .find-me { position: absolute; @@ -200,6 +188,14 @@ border-radius: 2px; } +.loading-spinner { + opacity: 0.5; + display: block; + margin: 2rem auto 0 auto; + margin-top: 2rem; + pointer-events: none; +} + .map-loading-spinner { position: absolute; top: 0; diff --git a/app/styles/modules/_m-noui.scss b/app/styles/modules/_m-noui.scss index 45b148049..a0e938dd4 100644 --- a/app/styles/modules/_m-noui.scss +++ b/app/styles/modules/_m-noui.scss @@ -16,8 +16,8 @@ &::before, &::after { - top: $slider-handle-height / 4; - height: $slider-handle-height / 2; + top: $slider-handle-height * 0.25; + height: $slider-handle-height * 0.5; left: 50%; margin-left: -2px; background: $light-gray; diff --git a/app/styles/modules/_m-search.scss b/app/styles/modules/_m-search.scss index d614dbb22..3d433a809 100644 --- a/app/styles/modules/_m-search.scss +++ b/app/styles/modules/_m-search.scss @@ -73,7 +73,7 @@ } li { - padding: $global-margin/2; + padding: $global-margin*0.5; } li:not(:first-child) { @@ -105,7 +105,7 @@ .search-results--loading { border-top: 1px solid $medium-gray; - padding: $global-margin/2; + padding: $global-margin*0.5; color: $dark-gray; background-color: rgba($white,0.94); font-size: rem-calc(12); diff --git a/app/templates/about.hbs b/app/templates/about.hbs index 84be817ad..12c4c9de2 100644 --- a/app/templates/about.hbs +++ b/app/templates/about.hbs @@ -1,9 +1,9 @@
To let
-
+
@NYCPlanningTech
know how this app could be better,
-
+
add a GitHub Issue
or send an email to
diff --git a/app/templates/application.hbs b/app/templates/application.hbs
index c4cb70cee..c929e8dfa 100644
--- a/app/templates/application.hbs
+++ b/app/templates/application.hbs
@@ -1,9 +1,9 @@
-
diff --git a/app/templates/components/bookmarks/bookmark-button.hbs b/app/templates/components/bookmarks/bookmark-button.hbs
index b9d6794dd..0cad4e515 100644
--- a/app/templates/components/bookmarks/bookmark-button.hbs
+++ b/app/templates/components/bookmarks/bookmark-button.hbs
@@ -1,12 +1,12 @@