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 @@ - + +
Homepage {{#if this.printSvc.enabled}} @@ -8,7 +9,17 @@ {{/if}}
{{outlet}} diff --git a/app/templates/bookmarks.hbs b/app/templates/bookmarks.hbs index e08e4d0b..c3ff9858 100644 --- a/app/templates/bookmarks.hbs +++ b/app/templates/bookmarks.hbs @@ -33,7 +33,7 @@ {{get (promise-rejected-reason this.bookmarksSettled) 'message'}}. {{/if}} - {{#unless this.model.length}} + {{#unless (or this.model.length this.savedLayerSets.length)}}

@@ -47,9 +47,65 @@

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 +

{{/unless}} +
+

+ Layer Sets + + Bookmark Current Layers + +

+ + {{#unless this.savedLayerSets.length}} +

No current saved layer sets.

+ {{/unless}} +
    + {{#each this.savedLayerSets as |bookmark|}} +
  • + {{#if (eq this.editMode bookmark.id)}} +
    + + + +
    + {{else}} + + + + + {{bookmark.name}} + + + {{/if}} +
  • + {{/each}} +
+
+ + {{outlet}}
\ No newline at end of file diff --git a/app/templates/components/default-modal.hbs b/app/templates/components/default-modal.hbs new file mode 100644 index 00000000..b2112af2 --- /dev/null +++ b/app/templates/components/default-modal.hbs @@ -0,0 +1,24 @@ + +

+ We're excited to share some new features we've added to ZoLa! Scroll through the updates below, or select the "Features" tab at any time to view the new additions. +

+ +
+ + + +
+ + {{input type="checkbox" checked=this.dontShowModalAgain}} + Don't show this message again + + {{!-- Uncomment the below to show a message only on mobile --}} + {{!--

+ labs-zola is optimized for desktop screen widths. +

+

+ Mobile support for ZoLa will be added in the future. + For the best experience, please use a browser with a minimum width of + 1280px. +

--}} +
\ No newline at end of file diff --git a/app/templates/components/layer-palette.hbs b/app/templates/components/layer-palette.hbs index 263b3572..628d9151 100644 --- a/app/templates/components/layer-palette.hbs +++ b/app/templates/components/layer-palette.hbs @@ -9,6 +9,23 @@ {{/if}}
+ {{#if this.showToggleLayersBackOn}} + + + Toggle Layers Back On + + {{else}} + + + Toggle All Map Layers Off + + {{/if}} - - - Reset Map Layers -
{{yield}} diff --git a/app/templates/components/layer-record-views/tax-lot.hbs b/app/templates/components/layer-record-views/tax-lot.hbs index acfe3a56..7de4ef15 100644 --- a/app/templates/components/layer-record-views/tax-lot.hbs +++ b/app/templates/components/layer-record-views/tax-lot.hbs @@ -1,4 +1,18 @@