diff --git a/app/components/default-modal.js b/app/components/default-modal.js
new file mode 100644
index 00000000..2ffc7f88
--- /dev/null
+++ b/app/components/default-modal.js
@@ -0,0 +1,31 @@
+import Component from '@ember/component';
+
+const TOTAL_SLIDES = 7;
+
+export default Component.extend({
+ tagName: '',
+ open: true,
+ slideNumber: Math.floor(Math.random() * TOTAL_SLIDES) + 1, // start on a random slide
+
+ actions: {
+ toggleModal() {
+ this.toggleProperty('open');
+ },
+ nextSlide() {
+ if (this.slideNumber < TOTAL_SLIDES) {
+ this.set('slideNumber', this.slideNumber + 1);
+ } else {
+ // comment out the line below to disable infinite looping
+ this.set('slideNumber', 1);
+ }
+ },
+ prevSlide() {
+ if (this.slideNumber > 1) {
+ this.set('slideNumber', this.slideNumber - 1);
+ } else {
+ // comment out the line below to disable infinite looping
+ this.set('slideNumber', TOTAL_SLIDES);
+ }
+ },
+ },
+});
diff --git a/app/components/layer-record-views/tax-lot.js b/app/components/layer-record-views/tax-lot.js
index 07c21eda..be0e6ad3 100644
--- a/app/components/layer-record-views/tax-lot.js
+++ b/app/components/layer-record-views/tax-lot.js
@@ -1,5 +1,8 @@
import carto from 'labs-zola/utils/carto';
import config from 'labs-zola/config/environment';
+import { inject as service } from '@ember/service';
+import { action } from '@ember/object';
+import bblDemux from 'labs-zola/utils/bbl-demux';
import LayerRecordComponent from './-base';
const { specialDistrictCrosswalk } = config;
@@ -310,6 +313,38 @@ const landuseLookup = {
};
export default class TaxLotRecordComponent extends LayerRecordComponent {
+ @service router;
+
+ @service mainMap;
+
+ @action
+ linkToLotComparison() {
+ this.router.transitionTo(
+ 'map-feature.lot-comparison',
+ this.model.borocode,
+ this.model.block,
+ this.model.lot,
+ 0,
+ 0,
+ 0
+ );
+ }
+
+ @action
+ removeLotFromComparison(otherModelId) {
+ this.set('mainMap.comparisonSelected', null);
+ const { boro, block, lot } = bblDemux(otherModelId);
+ this.router.transitionTo(
+ 'map-feature.lot-comparison',
+ boro,
+ block,
+ lot,
+ 0,
+ 0,
+ 0
+ );
+ }
+
get bldgclassname() {
return bldgclassLookup[this.model.bldgclass];
}
@@ -325,6 +360,13 @@ export default class TaxLotRecordComponent extends LayerRecordComponent {
return `${boroLookup[cdborocode]} Community District ${cd}`;
}
+ get boroSlashCd() {
+ const borocd = this.model.cd;
+ const cdborocode = `${borocd}`.substring(0, 1);
+ const cd = parseInt(`${borocd}`.substring(1, 3), 10).toString();
+ return `${boroLookup[cdborocode].replace(' ', '-').toLowerCase()}/${cd}`;
+ }
+
get cdURLSegment() {
const borocd = this.model.cd;
const borocode = this.model.borocode; // eslint-disable-line prefer-destructuring
@@ -333,6 +375,13 @@ export default class TaxLotRecordComponent extends LayerRecordComponent {
return `${cleanBorough}/${cd}`;
}
+ get googleMapsURL() {
+ const encodedAddress = encodeURIComponent(
+ `${this.model.address}, ${this.model.zipcode}`
+ );
+ return `https://www.google.com/maps/search/?api=1&query=${encodedAddress}`;
+ }
+
get landusename() {
return landuseLookup[this.model.landuse];
}
@@ -441,7 +490,9 @@ export default class TaxLotRecordComponent extends LayerRecordComponent {
}
get digitalTaxMapLink() {
- return `https://propertyinformationportal.nyc.gov/parcels/${this.model.condono ? 'condo' : 'parcel'}/${this.model.bbl}`;
+ return `https://propertyinformationportal.nyc.gov/parcels/${
+ this.model.condono ? 'condo' : 'parcel'
+ }/${this.model.bbl}`;
}
get zoningMapLink() {
diff --git a/app/components/main-header.js b/app/components/main-header.js
index 8fa3ebe4..bf83df43 100644
--- a/app/components/main-header.js
+++ b/app/components/main-header.js
@@ -1,10 +1,19 @@
import Component from '@ember/component';
import { inject as service } from '@ember/service';
+import { computed } from '@ember/object';
+import { tracked } from '@glimmer/tracking';
export default class MainHeaderComponent extends Component {
@service('print') printSvc;
@service() media;
- bookmarks;
+ @tracked bookmarks;
+
+ @tracked savedLayerSets;
+
+ @computed('bookmarks.length', 'savedLayerSets.length')
+ get totalBookmarks() {
+ return this.bookmarks.length + this.savedLayerSets.length;
+ }
}
diff --git a/app/components/main-map.js b/app/components/main-map.js
index 35f646ac..86ab5f7b 100644
--- a/app/components/main-map.js
+++ b/app/components/main-map.js
@@ -9,10 +9,14 @@ import { alias } from '@ember/object/computed';
import bblDemux from '../utils/bbl-demux';
import drawnFeatureLayers from '../layers/drawn-feature';
import selectedLayers from '../layers/selected-lot';
+import comparisonSelectedLayers from '../layers/comparison-selected-lot';
const selectedFillLayer = selectedLayers.fill;
const selectedLineLayer = selectedLayers.line;
+const comparisonSelectedFillLayer = comparisonSelectedLayers.fill;
+const comparisonSelectedLineLayer = comparisonSelectedLayers.line;
+
// Custom Control
const MeasurementText = function () {};
@@ -54,7 +58,7 @@ export default class MainMap extends Component {
highlightedLayerId = null;
- widowResize() {
+ windowResize() {
return new Promise((resolve) => {
setTimeout(() => {
const resizeEvent = window.document.createEvent('UIEvents');
@@ -114,6 +118,15 @@ export default class MainMap extends Component {
};
}
+ @computed('mainMap.comparisonSelected')
+ get comparisonSelectedLotSource() {
+ const comparisonSelected = this.get('mainMap.comparisonSelected');
+ return {
+ type: 'geojson',
+ data: comparisonSelected.get('geometry'),
+ };
+ }
+
@computed('mainMap.drawMode')
get interactivity() {
const drawMode = this.get('mainMap.drawMode');
@@ -124,6 +137,10 @@ export default class MainMap extends Component {
selectedLineLayer = selectedLineLayer;
+ comparisonSelectedFillLayer = comparisonSelectedFillLayer;
+
+ comparisonSelectedLineLayer = comparisonSelectedLineLayer;
+
@action
handleMapLoad(map) {
window.map = map;
@@ -211,7 +228,22 @@ export default class MainMap extends Component {
if (bbl && !ceqr_num) {
// eslint-disable-line
const { boro, block, lot } = bblDemux(bbl);
- this.router.transitionTo('map-feature.lot', boro, block, lot);
+ if (this.router.currentRoute.name === 'map-feature.lot-comparison') {
+ if (!this.mainMap.comparisonSelected) {
+ this.mainMap.set('comparisonSelected', this.mainMap.selected);
+ }
+ this.router.transitionTo(
+ 'map-feature.lot-comparison',
+ this.router.currentRoute.params.boro,
+ this.router.currentRoute.params.block,
+ this.router.currentRoute.params.lot,
+ boro,
+ block,
+ lot
+ );
+ } else {
+ this.router.transitionTo('map-feature.lot', boro, block, lot);
+ }
}
if (ulurpno) {
@@ -273,6 +305,6 @@ export default class MainMap extends Component {
this.set('printSvc.enabled', true);
- await this.widowResize();
+ await this.windowResize();
}
}
diff --git a/app/components/map-measurement-tools.js b/app/components/map-measurement-tools.js
index d1453c1e..30b92cdb 100644
--- a/app/components/map-measurement-tools.js
+++ b/app/components/map-measurement-tools.js
@@ -1,9 +1,72 @@
import Component from '@ember/component';
import numeral from 'numeral';
-import { action } from '@ember/object';
+import { action, computed } from '@ember/object';
import { inject as service } from '@ember/service';
import drawStyles from '../layers/draw-styles';
+function formatMeasurements(measurements) {
+ // metric calculation
+
+ let metricUnits = 'm';
+ let metricFormat = '0,0';
+ let metricMeasurement;
+
+ let standardUnits = 'feet';
+ let standardFormat = '0,0';
+ let standardMeasurement;
+
+ if (measurements.type === 'line') {
+ // user is drawing a line
+ metricMeasurement = measurements.metric;
+ if (measurements.metric >= 1000) {
+ // if over 1000 meters, upgrade metric
+ metricMeasurement = measurements.metric / 1000;
+ metricUnits = 'km';
+ metricFormat = '0.00';
+ }
+
+ standardMeasurement = measurements.standard;
+ if (standardMeasurement >= 5280) {
+ // if over 5280 feet, upgrade standard
+ standardMeasurement /= 5280;
+ standardUnits = 'mi';
+ standardFormat = '0.00';
+ }
+ } else {
+ // user is drawing a polygon
+ metricUnits = 'm²';
+ metricFormat = '0,0';
+ metricMeasurement = measurements.metric;
+
+ standardUnits = 'ft²';
+ standardFormat = '0,0';
+ standardMeasurement = measurements.standard;
+
+ if (measurements.metric >= 1000000) {
+ // if over 1,000,000 meters, upgrade metric
+ metricMeasurement = measurements.metric / 1000000;
+ metricUnits = 'km²';
+ metricFormat = '0.00';
+ }
+
+ if (standardMeasurement >= 27878400) {
+ // if over 27878400 sf, upgrade standard
+ standardMeasurement /= 27878400;
+ standardUnits = 'mi²';
+ standardFormat = '0.00';
+ }
+ }
+
+ const formattedMeasurements = {
+ metric: `${numeral(metricMeasurement).format(metricFormat)} ${metricUnits}`,
+ standard: `${numeral(standardMeasurement).format(
+ standardFormat
+ )} ${standardUnits}`,
+ };
+
+ return formattedMeasurements;
+}
+
export default class MapMeasurementToolsComponent extends Component {
@service mainMap;
@@ -13,6 +76,12 @@ export default class MapMeasurementToolsComponent extends Component {
drawnMeasurements = null;
+ previousStoredMeasurements = {
+ type: null,
+ metric: 0,
+ standard: 0,
+ };
+
measurementMenuOpen = false;
drawToolsOpen = false;
@@ -23,23 +92,36 @@ export default class MapMeasurementToolsComponent extends Component {
drawDidRender = false;
- drawnFeature = {
- type: 'Feature',
- geometry: null,
- };
+ drawnFeatures = [];
+
+ @computed(
+ 'drawnMeasurements.{metric,standard,type}',
+ 'previousStoredMeasurements.{metric,standard}'
+ )
+ get shownMeasurements() {
+ return formatMeasurements({
+ id: crypto.randomUUID(),
+ type: this.drawnMeasurements.type,
+ metric:
+ this.drawnMeasurements.metric + this.previousStoredMeasurements.metric,
+ standard:
+ this.drawnMeasurements.standard +
+ this.previousStoredMeasurements.standard,
+ });
+ }
@action
async startDraw(type) {
gtag('event', 'draw_tool', {
event_category: 'Measurement',
- event_action: 'Used measurement tool',
+ event_action: `Measurement #${this.drawnFeatures.length}`,
});
// GA
this.metrics.trackEvent('MatomoTagManager', {
category: 'Measurement',
action: 'Used measurement tool',
- name: 'Measurement',
+ name: `Measurement #${this.drawnFeatures.length}`,
});
this.set('didStartDraw', true);
@@ -55,12 +137,8 @@ export default class MapMeasurementToolsComponent extends Component {
this.set('draw', draw);
const drawMode = type === 'line' ? 'draw_line_string' : 'draw_polygon';
const { mainMap } = this;
- if (mainMap.get('drawMode')) {
- draw.deleteAll();
- } else {
+ if (!mainMap.get('drawMode')) {
mainMap.mapInstance.addControl(draw);
- this.set('drawnFeature', null);
- this.set('drawnMeasurements', null);
}
mainMap.set('drawMode', drawMode);
draw.changeMode(drawMode);
@@ -75,18 +153,36 @@ export default class MapMeasurementToolsComponent extends Component {
}
mainMap.set('drawMode', null);
- this.set('drawnFeature', null);
+ this.set('drawnFeatures', []);
this.set('drawnMeasurements', null);
+ this.set('previousStoredMeasurements', { metric: 0, standard: 0 });
}
@action
handleDrawCreate(e) {
const { draw } = this;
- this.set('drawnFeature', e.features[0].geometry);
+
+ this.set('drawnFeatures', [
+ ...this.drawnFeatures,
+ { ...e.features[0].geometry, id: crypto.randomUUID() },
+ ]);
+ this.set('previousStoredMeasurements', {
+ type: this.drawnMeasurements.type,
+ metric:
+ this.drawnMeasurements.metric + this.previousStoredMeasurements.metric,
+ standard:
+ this.drawnMeasurements.standard +
+ this.previousStoredMeasurements.standard,
+ });
setTimeout(() => {
if (!this.mainMap.isDestroyed && !this.mainMap.isDestroying) {
this.mainMap.mapInstance.removeControl(draw);
this.mainMap.set('drawMode', null);
+ this.set('drawnMeasurements', {
+ type: this.drawnMeasurements.type,
+ metric: 0,
+ standard: 0,
+ });
}
}, 100);
}
@@ -123,61 +219,26 @@ async function calculateMeasurements(feature) {
const drawnLength = lineDistance(feature) * 1000; // meters
const drawnArea = area(feature); // square meters
- let metricUnits = 'm';
- let metricFormat = '0,0';
+ let featureType;
let metricMeasurement;
-
- let standardUnits = 'feet';
- let standardFormat = '0,0';
let standardMeasurement;
if (drawnLength > drawnArea) {
// user is drawing a line
metricMeasurement = drawnLength;
- 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
- standardMeasurement /= 5280;
- standardUnits = 'mi';
- standardFormat = '0.00';
- }
+ featureType = 'line';
} else {
// user is drawing a polygon
- metricUnits = 'm²';
- metricFormat = '0,0';
metricMeasurement = drawnArea;
-
- standardUnits = 'ft²';
- standardFormat = '0,0';
standardMeasurement = drawnArea * 10.7639;
-
- 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
- standardMeasurement /= 27878400;
- standardUnits = 'mi²';
- standardFormat = '0.00';
- }
+ featureType = 'polygon';
}
const drawnMeasurements = {
- metric: `${numeral(metricMeasurement).format(metricFormat)} ${metricUnits}`,
- standard: `${numeral(standardMeasurement).format(
- standardFormat
- )} ${standardUnits}`,
+ metric: metricMeasurement,
+ standard: standardMeasurement,
+ type: featureType,
};
return drawnMeasurements;
diff --git a/app/components/map-resource-search.js b/app/components/map-resource-search.js
index 89f3105e..c6610b11 100644
--- a/app/components/map-resource-search.js
+++ b/app/components/map-resource-search.js
@@ -74,9 +74,24 @@ 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 },
- });
+ if (this.router.currentRoute.name === 'map-feature.lot-comparison') {
+ this.router.transitionTo(
+ 'map-feature.lot-comparison',
+ this.router.currentRoute.params.boro,
+ this.router.currentRoute.params.block,
+ this.router.currentRoute.params.lot,
+ boro,
+ block,
+ lot,
+ {
+ queryParams: { search: true },
+ }
+ );
+ } else {
+ this.router.transitionTo('map-feature.lot', boro, block, lot, {
+ queryParams: { search: true },
+ });
+ }
}
if (type === 'zma') {
diff --git a/app/components/mapbox/map-feature-renderer.js b/app/components/mapbox/map-feature-renderer.js
index 8039c2db..86ab4a6f 100644
--- a/app/components/mapbox/map-feature-renderer.js
+++ b/app/components/mapbox/map-feature-renderer.js
@@ -7,12 +7,35 @@ export default class MapboxMapFeatureRenderer extends Component {
@service mainMap;
+ @service router;
+
// this is usually a query param, which comes through a string.
shouldFitBounds = true;
didInsertElement(...args) {
super.didInsertElement(...args);
- this.setSelectedFeature(this.model);
+
+ // if they match it's the selection
+ // if the boro is 0 then there's no comparison area selected yet
+ // otherwise, it's the comparison selection
+ if (
+ this.model.properties.borocode ===
+ parseInt(this.router.currentRoute.params.boro, 10) &&
+ this.model.properties.block ===
+ parseInt(this.router.currentRoute.params.block, 10) &&
+ this.model.properties.lot ===
+ parseInt(this.router.currentRoute.params.lot, 10)
+ ) {
+ this.setSelectedFeature(this.model);
+ } else if (
+ this.router.currentRoute.params.comparisonboro !== '0' &&
+ this.router.currentRoute.name === 'map-feature.lot-comparison'
+ ) {
+ this.setComparisonSelectedFeature(this.model);
+ } else {
+ this.setSelectedFeature(this.model);
+ this.setComparisonSelectedFeature(null);
+ }
if (this.shouldFitBounds) {
this.setFitBounds(this.model);
@@ -27,4 +50,8 @@ export default class MapboxMapFeatureRenderer extends Component {
setSelectedFeature(model) {
this.set('mainMap.selected', model);
}
+
+ setComparisonSelectedFeature(model) {
+ this.set('mainMap.comparisonSelected', model);
+ }
}
diff --git a/app/components/print-view-controls.js b/app/components/print-view-controls.js
index d25f486f..75acfe31 100644
--- a/app/components/print-view-controls.js
+++ b/app/components/print-view-controls.js
@@ -9,7 +9,7 @@ export default class PrintViewControls extends Component {
@service metrics;
- widowResize() {
+ windowResize() {
return new Promise((resolve) => {
setTimeout(() => {
const resizeEvent = window.document.createEvent('UIEvents');
@@ -35,10 +35,10 @@ export default class PrintViewControls extends Component {
this.set('printSvc.enabled', false);
- await this.widowResize();
+ await this.windowResize();
}
async click() {
- await this.widowResize();
+ await this.windowResize();
}
}
diff --git a/app/controllers/application.js b/app/controllers/application.js
index f42ae2e3..d85a094b 100644
--- a/app/controllers/application.js
+++ b/app/controllers/application.js
@@ -1,6 +1,7 @@
import Controller from '@ember/controller';
import { assign } from '@ember/polyfills';
import { computed, action } from '@ember/object';
+import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import QueryParams from '@nycplanning/ember-parachute';
import config from 'labs-zola/config/environment';
@@ -70,7 +71,7 @@ export const mapQueryParams = new QueryParams(
},
'aerial-year': {
- defaultValue: 'aerials-2016',
+ defaultValue: 'aerials-2022',
},
// TODO: After merge of params refactor, update print service based on this param.
@@ -87,6 +88,23 @@ export default class ApplicationController extends Controller.extend(
@service mainMap;
+ @service metrics;
+
+ @tracked leftSideMenuVisibilty = true;
+
+ @tracked layerGroupsStorage;
+
+ windowResize() {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ const resizeEvent = window.document.createEvent('UIEvents');
+ resizeEvent.initUIEvent('resize', true, false, window, 0);
+ window.dispatchEvent(resizeEvent);
+ resolve();
+ }, 1);
+ });
+ }
+
// this action extracts query-param-friendly state of layer groups
// for various paramable layers
@action
@@ -98,6 +116,7 @@ export default class ApplicationController extends Controller.extend(
.sort();
this.set('layerGroups', visibleLayerGroups);
+ this.set('layerGroupsStorage', null);
}
@action
@@ -106,10 +125,115 @@ export default class ApplicationController extends Controller.extend(
this.handleLayerGroupChange();
}
+ @action
+ setAllLayerVisibilityToFalse() {
+ // save them so we can be able to reset them
+ const tempStorage = this.model.layerGroups
+ .filter(({ visible }) => visible)
+ .map(({ id }) => id)
+ .sort();
+
+ this.model.layerGroups
+ .filter(({ visible }) => visible)
+ .forEach((model) => this.toggleLayerVisibilityToFalse(model));
+ this.handleLayerGroupChange();
+
+ this.set('layerGroupsStorage', tempStorage);
+
+ gtag('event', 'search', {
+ event_category: 'Toggle Layer',
+ event_action: 'Toggle All Layers Off',
+ });
+
+ // GA
+ this.metrics.trackEvent('MatomoTagManager', {
+ category: 'Toggle Layer',
+ action: 'Toggle All Layers Off',
+ name: 'Toggle All Layers Off',
+ });
+ }
+
+ @action
+ undoSetAllLayerVisibilityToFalse() {
+ this.model.layerGroups.forEach((lg) => {
+ if (this.layerGroupsStorage.includes(lg.id)) {
+ lg.set('visible', true);
+ }
+ });
+
+ this.set('layerGroupsStorage', null);
+ this.handleLayerGroupChange();
+
+ gtag('event', 'search', {
+ event_category: 'Toggle Layer',
+ event_action: 'Undo Toggle All Layers Off',
+ });
+
+ // GA
+ this.metrics.trackEvent('MatomoTagManager', {
+ category: 'Toggle Layer',
+ action: 'Undo Toggle All Layers Off',
+ name: 'Undo Toggle All Layers Off',
+ });
+ }
+
+ @action
+ toggleLayerVisibilityToFalse(layer) {
+ layer.visible = false;
+ }
+
+ @computed('layerGroupsStorage', 'model.layerGroups')
+ get showToggleLayersBackOn() {
+ if (
+ this.model.layerGroups.filter(({ visible }) => visible).length === 0 &&
+ this.layerGroupsStorage
+ ) {
+ return true;
+ }
+ return false;
+ }
+
@computed('queryParamsState')
get isDefault() {
const state = this.queryParamsState || {};
const values = Object.values(state);
return values.every(({ changed }) => changed === false);
}
+
+ @tracked
+ openModal = !window.localStorage.hideMessage;
+
+ @tracked
+ dontShowModalAgain = false;
+
+ @action toggleModal() {
+ this.openModal = !this.openModal;
+ if (this.dontShowModalAgain) {
+ window.localStorage.hideMessage = true;
+ }
+ }
+
+ @action
+ async toggleLeftSideMenuVisibility() {
+ this.leftSideMenuVisibilty = !this.leftSideMenuVisibilty;
+
+ const mapContainer = document.querySelector('.map-container');
+
+ if (this.leftSideMenuVisibilty)
+ mapContainer.setAttribute('class', 'map-container');
+ else mapContainer.setAttribute('class', 'map-container full-width');
+
+ await this.windowResize();
+
+ this.metrics.trackEvent('MatomoTagManager', {
+ category: 'Toggled Layer Menu Visibility',
+ action: 'Toggled Layer Menu Visibility',
+ name: `${this.leftSideMenuVisibilty ? 'Opened' : 'Closed'}`,
+ });
+
+ gtag('event', 'toggle_menu', {
+ event_category: 'Toggled Layer Menu Visibility',
+ event_action: `${this.leftSideMenuVisibilty ? 'Opened' : 'Closed'}`,
+ });
+ }
}
diff --git a/app/controllers/bookmarks.js b/app/controllers/bookmarks.js
index 67dff1ed..46b7e214 100644
--- a/app/controllers/bookmarks.js
+++ b/app/controllers/bookmarks.js
@@ -1,11 +1,40 @@
+/* eslint-disable no-unused-expressions */
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { computed as computedProp } from '@ember/object';
import { Promise } from 'rsvp';
+const QUERY_PARAMS_LIST = [
+ 'selectedZoning',
+ 'selectedOverlays',
+ 'selectedFirm',
+ 'selectedPfirm',
+ 'selectedCouncilDistricts',
+ 'selectedLayerGroup',
+];
+
export default Controller.extend({
mainMap: service(),
metrics: service(),
+ router: service(),
+
+ savedLayerSets: window.localStorage['saved-layer-sets']
+ ? JSON.parse(window.localStorage['saved-layer-sets'])
+ : [],
+
+ editMode: false,
+
+ track(act) {
+ gtag('event', 'saved_layer_sets', {
+ event_category: 'Saved Layer Sets',
+ event_action: act,
+ });
+ this.metrics.trackEvent('MatomoTagManager', {
+ category: 'Saved Layer Sets',
+ action: act,
+ name: act,
+ });
+ },
// because we must compute the record types based on multiple
// promises, the model uses Promise.all
// this gets us in trouble when we need to do
@@ -26,5 +55,100 @@ export default Controller.extend({
zoom: 15,
});
},
+
+ bookmarkCurrentLayerSet() {
+ let allLayers = [];
+ const visibleLayers = [];
+ const visibleLayerGroups = [];
+ this.router.currentRoute.parent.attributes.layerGroups.forEach((lg) => {
+ allLayers = allLayers.concat(lg.layers);
+ lg.visible ? visibleLayerGroups.push(lg.id) : null;
+ });
+ allLayers.forEach((layer) => {
+ layer.visibility ? visibleLayers.push(layer.id) : null;
+ });
+
+ const queryParams = {};
+ ['layer-groups', ...QUERY_PARAMS_LIST].forEach((selected) => {
+ queryParams[selected] = this.router.currentRoute.queryParams[selected]
+ ? JSON.parse(this.router.currentRoute.queryParams[selected])
+ : undefined;
+ });
+
+ const layerSet = {
+ id: crypto.randomUUID(),
+ name: 'New Saved Layer Set',
+ visibleLayers,
+ visibleLayerGroups,
+ queryParams,
+ };
+ this.set('savedLayerSets', [...this.savedLayerSets, layerSet]);
+ window.localStorage['saved-layer-sets'] = JSON.stringify(
+ this.savedLayerSets
+ );
+ this.track('bookmarkCurrentLayerSet');
+ // Hack to update the # which doesn't update automatically
+ document.querySelector('.badge.sup').innerText =
+ parseInt(document.querySelector('.badge.sup').innerText, 10) + 1;
+ },
+
+ deleteBookmarkedLayerSettings(id) {
+ this.set(
+ 'savedLayerSets',
+ [...this.savedLayerSets].filter((lg) => lg.id !== id)
+ );
+ window.localStorage['saved-layer-sets'] = JSON.stringify(
+ this.savedLayerSets
+ );
+ this.track('deleteBookmarkedLayerSettings');
+ // Hack to update the # which doesn't update automatically
+ document.querySelector('.badge.sup').innerText =
+ parseInt(document.querySelector('.badge.sup').innerText, 10) - 1;
+ },
+
+ updateBookmarkedLayerSettings(id) {
+ const newLayerSets = [...this.savedLayerSets];
+ const updatedLayerSetIndex = newLayerSets.findIndex((lg) => lg.id === id);
+ newLayerSets[updatedLayerSetIndex].name =
+ document.getElementById('name').value;
+ this.set('savedLayerSets', newLayerSets);
+ window.localStorage['saved-layer-sets'] = JSON.stringify(
+ this.savedLayerSets
+ );
+ this.set('editMode', false);
+ // without the below, the name won't update in the dom
+ setTimeout(function () {
+ document.getElementById(id).innerText =
+ newLayerSets[updatedLayerSetIndex].name;
+ }, 1);
+ this.track('finishUpdateBookmarkedLayerSettings');
+ },
+
+ turnOnEditMode(id) {
+ this.set('editMode', id);
+ this.track('beginUpdateBookmarkedLayerSettings');
+ },
+
+ loadBookmarkedLayerSettings(bookmarkId) {
+ const layerToLoad = this.savedLayerSets.find(
+ (lg) => bookmarkId === lg.id
+ );
+ const layerGroups = [
+ ...this.router.currentRoute.parent.attributes.layerGroups,
+ ];
+ layerGroups.forEach((lg) => {
+ lg.visible = !!layerToLoad.visibleLayerGroups.includes(lg.id);
+ lg.layers.forEach((layer) => {
+ layer.visibility = !!layerToLoad.visibleLayers.includes(layer.id);
+ });
+ });
+
+ QUERY_PARAMS_LIST.forEach((selected) => {
+ this.router.currentRoute.queryParams[selected] =
+ layerToLoad.queryParams[selected];
+ });
+
+ this.track('loadBookmarkedLayerSettings');
+ },
},
});
diff --git a/app/controllers/map-feature.js b/app/controllers/map-feature.js
index 94e76d28..89db0bc4 100644
--- a/app/controllers/map-feature.js
+++ b/app/controllers/map-feature.js
@@ -1,6 +1,10 @@
import Controller from '@ember/controller';
+import { inject as service } from '@ember/service';
+import { action } from '@ember/object';
export default class MapFeatureController extends Controller {
+ @service mainMap;
+
queryParams = [
{
search: {
@@ -11,4 +15,9 @@ export default class MapFeatureController extends Controller {
];
shouldRefresh = false;
+
+ @action
+ clearComparison() {
+ this.set('mainMap.comparisonSelected', null);
+ }
}
diff --git a/app/layers/comparison-selected-lot.js b/app/layers/comparison-selected-lot.js
new file mode 100644
index 00000000..817c02d7
--- /dev/null
+++ b/app/layers/comparison-selected-lot.js
@@ -0,0 +1,32 @@
+const comparisonSelectedLayers = {
+ fill: {
+ id: 'comparison-selected-fill',
+ type: 'fill',
+ source: 'comparison-selected-lot',
+ paint: {
+ 'fill-opacity': 0.6,
+ 'fill-color': 'rgba(0, 20, 130, 1)',
+ },
+ },
+ line: {
+ id: 'comparison-selected-line',
+ type: 'line',
+ source: 'comparison-selected-lot',
+ layout: {
+ 'line-cap': 'round',
+ },
+ paint: {
+ 'line-opacity': 0.9,
+ 'line-color': 'rgba(0, 10, 90, 1)',
+ 'line-width': {
+ stops: [
+ [13, 1.5],
+ [15, 8],
+ ],
+ },
+ 'line-dasharray': [2, 1.5],
+ },
+ },
+};
+
+export default comparisonSelectedLayers;
diff --git a/app/layers/drawn-feature.js b/app/layers/drawn-feature.js
index 511671f0..9a05f24a 100644
--- a/app/layers/drawn-feature.js
+++ b/app/layers/drawn-feature.js
@@ -1,6 +1,5 @@
export default {
line: {
- id: 'drawn-feature-line',
type: 'line',
source: 'drawn-feature',
paint: {
@@ -11,7 +10,6 @@ export default {
},
},
fill: {
- id: 'drawn-feature-fill',
type: 'fill',
source: 'drawn-feature',
paint: {
diff --git a/app/models/lot-comparison.js b/app/models/lot-comparison.js
new file mode 100644
index 00000000..1283c2c1
--- /dev/null
+++ b/app/models/lot-comparison.js
@@ -0,0 +1,14 @@
+import { attr } from '@ember-data/model';
+import CartoGeojsonFeature from './carto-geojson-feature';
+
+export default class LotComparison extends CartoGeojsonFeature {
+ @attr properties;
+
+ get title() {
+ return this.get('properties.address');
+ }
+
+ get subtitle() {
+ return this.get('properties.bbl');
+ }
+}
diff --git a/app/router.js b/app/router.js
index 070a1ce1..c1507507 100644
--- a/app/router.js
+++ b/app/router.js
@@ -22,6 +22,9 @@ Router.map(function () {// eslint-disable-line
// views for individual records of data
this.route('map-feature', { path: '/l' }, function () {
this.route('lot', { path: 'lot/:boro/:block/:lot' });
+ this.route('lot-comparison', {
+ path: 'lot-comparison/:boro/:block/:lot/:comparisonboro/:comparisonblock/:comparisonlot/',
+ });
this.route('zoning-district', { path: '/zoning-district/:id' });
this.route('commercial-overlay', { path: '/commercial-overlay/:id' });
this.route('special-purpose-district', {
diff --git a/app/routes/application.js b/app/routes/application.js
index 1abb8f6d..619cf059 100644
--- a/app/routes/application.js
+++ b/app/routes/application.js
@@ -28,7 +28,10 @@ export default Route.extend({
this.router.transitionTo(`/about${transition.intent.url}`);
}
- if (targetName === 'map-feature.lot') {
+ if (
+ targetName === 'map-feature.lot' ||
+ targetName === 'map-feature.lot-comparison'
+ ) {
this.set('mainMap.routeIntentIsNested', true);
}
},
@@ -58,11 +61,16 @@ export default Route.extend({
await bookmarks.invoke('get', 'bookmark');
+ const savedLayerSets = window.localStorage['saved-layer-sets']
+ ? JSON.parse(window.localStorage['saved-layer-sets'])
+ : [];
+
return {
layerGroups,
layerGroupsObject,
meta,
bookmarks,
+ savedLayerSets,
};
},
});
diff --git a/app/routes/map-feature/lot-comparison.js b/app/routes/map-feature/lot-comparison.js
new file mode 100644
index 00000000..9d10d524
--- /dev/null
+++ b/app/routes/map-feature/lot-comparison.js
@@ -0,0 +1,24 @@
+import Route from '@ember/routing/route';
+import bblDemux, { comparisonBblDemux } from 'labs-zola/utils/bbl-demux';
+import { action } from '@ember/object';
+import { inject as service } from '@ember/service';
+
+export default class MapFeatureLotComparisonRoute extends Route {
+ @service router;
+
+ model(params) {
+ const id = bblDemux(params);
+ const comparisonid = comparisonBblDemux(params);
+
+ return {
+ id,
+ comparisonid,
+ };
+ }
+
+ @action
+ cancelLotComparison(id) {
+ const { boro, block, lot } = bblDemux(id);
+ this.router.transitionTo('map-feature.lot', boro, block, lot);
+ }
+}
diff --git a/app/services/main-map.js b/app/services/main-map.js
index 09581f49..e652f42a 100644
--- a/app/services/main-map.js
+++ b/app/services/main-map.js
@@ -20,6 +20,8 @@ export default class MainMapService extends Service {
// used to determine how to zoom
selected = null;
+ comparisonSelected = null;
+
currentMeasurement = null;
drawMode = null;
diff --git a/app/styles/app.scss b/app/styles/app.scss
index d0865094..c9833f87 100644
--- a/app/styles/app.scss
+++ b/app/styles/app.scss
@@ -21,6 +21,7 @@
@include foundation-close-button;
@include foundation-menu;
@include foundation-label;
+@include foundation-reveal;
@include foundation-switch;
@include foundation-visibility-classes;
@include foundation-float-classes;
@@ -46,7 +47,7 @@
@import 'modules/_m-maps';
@import 'modules/_m-noui';
@import 'modules/_m-search';
-
+@import 'modules/_m-reveal-modal';
@import 'ember-power-select';
@import 'layouts/_l-print';
diff --git a/app/styles/layouts/_l-default.scss b/app/styles/layouts/_l-default.scss
index afddf475..c22ef56e 100644
--- a/app/styles/layouts/_l-default.scss
+++ b/app/styles/layouts/_l-default.scss
@@ -95,6 +95,11 @@ body {
@include breakpoint(xlarge) {
width: calc(100% - 18rem);
}
+
+ &.full-width {
+ width: 100%;
+ }
+
}
body.index {
@@ -193,7 +198,7 @@ body.index {
);
}
- & > .close-button {
+ & .close-button {
position: relative;
margin: 0;
@@ -214,6 +219,50 @@ body.index {
}
}
+.content-toggle-layer-palette-container {
+ position: relative;
+ z-index: 3;
+ box-shadow: 0 2px 0 rgba(0,0,0,0.1);
+ background-color: $white;
+ text-align: left;
+
+ @include breakpoint(small down) {
+ display: none;
+ }
+
+ @include breakpoint(medium down) {
+ @include xy-cell(
+ $size: full,
+ $output: (base size),
+ $gutters: 0
+ );
+ }
+
+ & > .close-button {
+ position: relative;
+ margin: 0;
+ max-inline-size: max-content;
+ border: 2px solid rgba(0,0,0,0.25);
+ border-left: none;
+ border-top-right-radius: 5px;
+ border-bottom-right-radius: 5px;
+
+ @include breakpoint(large) {
+ display: block;
+ position: fixed;
+ z-index: 3;
+ top: 50%;
+ left: 18rem;
+ background-color: $white;
+ margin-left: -4px;
+ padding: 0 rem-calc(6) rem-calc(6);
+ }
+ @include breakpoint(xxlarge) {
+ left: 18rem;
+ }
+ }
+}
+
.content-is-closed {
.navigation-area {
@@ -266,3 +315,17 @@ body.index {
left: 50%;
}
}
+
+.add-comparison-lot-button {
+ border: 1px solid;
+ border-color: green;
+ border-radius: 5px;
+ padding: .5rem;
+ margin-bottom: .5rem;
+ color: green;
+ &:hover {
+ background-color: green;
+ border: 1px solid;
+ color: white;
+ }
+}
\ No newline at end of file
diff --git a/app/styles/layouts/_l-print.scss b/app/styles/layouts/_l-print.scss
index 5ddc940c..8a17104a 100644
--- a/app/styles/layouts/_l-print.scss
+++ b/app/styles/layouts/_l-print.scss
@@ -154,6 +154,7 @@
// --------------------------------------------------
.hide-for-print, // Hide everything that doesn't print
.content-close-button-container,
+ .content-toggle-layer-palette-container,
.layer-groups-container:not(.has-active-layer-groups),
.layer-groups-container:not(.open),
.layer-groups-container-title > .badge,
diff --git a/app/styles/modules/_m-layer-palette.scss b/app/styles/modules/_m-layer-palette.scss
index 5655eebe..cd4d8a82 100644
--- a/app/styles/modules/_m-layer-palette.scss
+++ b/app/styles/modules/_m-layer-palette.scss
@@ -131,6 +131,13 @@ $layer-palette-hover-color: rgba($dark-gray,0.08);
width: calc(100% - #{$layer-palette-padding*4});
}
+//
+// "Toggle All Map Layers Off" button
+// --------------------------------------------------
+.no-layers-button {
+ margin: $layer-palette-padding*3 $layer-palette-padding*2 $layer-palette-padding*2 $layer-palette-padding*2;
+ width: calc(100% - #{$layer-palette-padding*4});
+}
//
// Indeterminate hider (hides element alongside unchecked checkbox)
diff --git a/app/styles/modules/_m-reveal-modal.scss b/app/styles/modules/_m-reveal-modal.scss
new file mode 100644
index 00000000..c4997975
--- /dev/null
+++ b/app/styles/modules/_m-reveal-modal.scss
@@ -0,0 +1,97 @@
+// --------------------------------------------------
+// Module: Reveal Modal
+// --------------------------------------------------
+
+#reveal-modal-container {
+ z-index: 3;
+}
+
+@include breakpoint(1280px up) {
+ .mobile-header,
+ .mobile-body {
+ display: none;
+ }
+}
+
+
+.reveal-overlay {
+ padding: 1rem;
+
+ @include breakpoint(medium) {
+ padding: 8rem 1rem;
+ // position: absolute;
+ }
+}
+
+.reveal-overlay-target {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+}
+
+.reveal {
+ border: 0;
+ box-shadow: 0 0 0 rem-calc(4) rgba(0,0,0,0.1);
+ top: 0;
+ border-radius: 5px;
+ padding-top: 40px;
+
+ &:focus {
+ outline: none;
+ }
+}
+
+/* Slideshow container */
+.slideshow-container {
+ position: relative;
+ /* should we keep this? */
+ min-height: 10rem;
+}
+
+/* Next & previous buttons */
+.slideshow-prev, .slideshow-next {
+ cursor: pointer;
+ position: absolute;
+ top: 50%;
+ width: auto;
+ padding: 16px;
+ margin-top: -22px;
+ color: white;
+ font-weight: bold;
+ font-size: 48px;
+ transition: 0.6s ease;
+ border-radius: 0 3px 3px 0;
+ user-select: none;
+ background-color: rgba(0,0,0,0.1);
+}
+
+/* Position the "next button" to the right */
+.slideshow-next {
+ right: 0;
+ border-radius: 3px 0 0 3px;
+}
+
+/* On hover, add a black background color with a little bit see-through */
+.slideshow-prev:hover, .slideshow-next:hover {
+ background-color: rgba(0,0,0,0.8);
+}
+
+.slideshow-content {
+ text-align: center;
+ margin-bottom: 1rem;
+}
+
+.slide {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ padding-top: 1rem;
+ background-color: lightgray;
+}
+
+.slide img {
+ height: 350px;
+}
\ No newline at end of file
diff --git a/app/templates/application.hbs b/app/templates/application.hbs
index c929e8df..0222c4cc 100644
--- a/app/templates/application.hbs
+++ b/app/templates/application.hbs
@@ -1,4 +1,5 @@
-
From this page you can quickly navigate to all of your bookmarked information.
++ If you would like to bookmark the current selected set of layers, use the button below. +
+ + Bookmark Current Layers +No current saved layer sets.
+ {{/unless}} ++ Mobile support for ZoLa will be added in the future. + For the best experience, please use a browser with a minimum width of + 1280px. +
--}} +