diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..27bbca74 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v12.22.1 diff --git a/README.md b/README.md index da5f6603..52129761 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,3 @@ -[![CircleCI](https://circleci.com/gh/NYCPlanning/labs-citymap/tree/develop.svg?style=svg)](https://circleci.com/gh/NYCPlanning/labs-citymap/tree/develop) - # The City Map The City Map is the official adopted map of the city. It shows the location, dimension and grades of streets, parks, public places, and certain public easements. However, the City Map isn’t one comprehensive map; it’s a collection of many alteration maps for small areas of the City. This app brings all of the pieces of the City Map into one convenient place on the web. @@ -10,8 +8,6 @@ The City Map is the official adopted map of the city. It shows the location, dim [NYC Planning Labs](https://planninglabs.nyc) takes on a single project at a time, working closely with our customers from concept to delivery in a matter of weeks. We conduct regular maintenance between larger projects. -Take a look at our [sprint planning board](https://waffle.io/NYCPlanning/labs-citymap) to get an idea of our current priorities for this project. - ## How you can help In the spirit of free software, everyone is encouraged to help improve this project. Here are some ways you can contribute. @@ -33,8 +29,7 @@ In the spirit of free software, everyone is encouraged to help improve this proj You will need the following things properly installed on your computer. - [Git](https://git-scm.com/) -- [Node.js](https://nodejs.org/) (with NPM) - - This installation was tested using Node v12.22.1 +- [Node.js](https://nodejs.org/) (with NPM) **version listed in .nvmrc** - [Ember CLI](https://ember-cli.com/) - [Yarn](https://yarnpkg.com/) @@ -43,25 +38,24 @@ You will need the following things properly installed on your computer. - Clone this repo `git clone git@github.com:NYCPlanning/labs-streets.git` - Navigate to the repo: `cd labs-streets` - Install Dependencies `yarn` -- Start the server `ember s` +- Start the server `yarn run start` + +## Configuring Carto instance for ad-hoc queries +The Carto instance to be used when the app calls Carto's [SQL API](https://carto.com/developers/sql-api/) is controlled by the `carto-username` key defined on the `ENV` object in `/config/environment.js`. When you run the project locally, by default, that should point to the `planninglabs` instance. To change this, you can temporarily change the `carto-username` value in `environment.js` or pass in a `CARTO_USER` environment variable when you run the project (See `netlify.toml` for an example of how to do this) -## Connecting to local Layers API +## Configuring which Layers API environment to use -By default, in development this app will acquire Layer Group information from the static file at `public/layer-groups/v1`. +This application uses [Layers API](https://github.com/NYCPlanning/labs-layers-api/) to pull down JSON configurations that are used to call Carto's Maps API to serve vector tiles to the client. The base URL specified in the `host` key on `ENV` in `/config/environment.js` determines which environment of Layers API you will be pointing to when running this project locally. By default, the app will point to the production layers API. To change this, you can temporarily update the `host` value on the `ENV` object in `environment.js` or pass in a `API_HOST` environment variable when you run the project (see `netlify.toml` for examples of how to do this). -To retrieve the latest layer group info by targetting a locally run or live Layers API, you can specify the `host` environment variable -within `config/environment.js`. +If you are also running Layers API locally and want to use that, you can specify the localhost with the port for `host`, such as -For example, if you are running a local Layers API at `localhost:3000`, then under the `environment === 'development'` condition in `config/envrinment.js`, add ``` ENV.host = 'http://localhost:3000'; ``` ## Architecture -City Map is an [Ember.js](https://www.emberjs.com/) single page application (SPA). The frontend handles routing, web mapping, layout, and user interactions, and communicates with various APIs for content and data. - -Several dependencies are split up into their own repositories under /lib, including labs-layers, labs-ember-search, and cartobox-promises-utility. These are starting points for possible addons. +Streets is an [Ember.js](https://www.emberjs.com/) single page application (SPA). The frontend handles routing, web mapping, layout, and user interactions, and communicates with various APIs for content and data. ### Models, Layers, and Mutability In this mapping iteration, we use models to manage individual layer and layer-group state. A layer group is a collection of layers, with a single visibility state. If a layer group's visibility is toggled off, that state is delegated to its related layers. These models allow for developers to reference a layer model from anywhere and manipulate its state, including layers' paint, layout, and filter states. These three properties are part of the MapboxGL Style Specification. @@ -124,17 +118,13 @@ layerModelInstance.setFilter(['all', ['>=', 'effective', 100]]); To style the map in development, we use [Maputnik Dev Server](https://github.com/NYCPlanning/labs-maputnik-dev-server). Check the `README` of maputnik-dev-server for the commands necessary to style the current map. -#### Layer-Groups - -{TODO: explain how Layer Groups work} - ## Backend services Carto is the primary backend resource, which provides a PostGIS database, Map Tiler, and JSON/GeoJSON data API. All of the data represented in the app starts out as a PostGIS table in Carto. - **Carto Maps API** - Spatial data used in the map are served as vector tiles from the Carto Maps API. Vector tiles are defined by a SQL query, and may include several named internal layers. Vector Tiles are defined in config files in app/sources, and the frontend converts each of these into a call to the Maps API, which produces a vector tile template that can be added to Mapbox GL as a 'source'. - **Carto SQL API** - The SQL API is used to retrieve record-specific data, such as tax lot details when a user navigates to a specific lot. It is also used to cross-reference a selected lot on-the-fly with various other layers to find intersections. -- **Search** - {TODO: description of this service} +- **Search** - The typeahead search feature in the upper left of the UI is powered by [Search API](https://github.com/NYCPlanning/labs-search-api). The configuration for these integration is defined by the `labs-search` object found in `/config/environment.js` ## Testing and checks @@ -146,57 +136,6 @@ Carto is the primary backend resource, which provides a PostGIS database, Map Ti - run `ember test --serve` - Before creating a Pull Request, make sure your branch is updated with the latest `develop` and passes all tests -## Deployment - -The App is deployed to our VPS using Dokku, you need only do a `git push` of the master branch to the `dokku` remote. - -- To create a new remote named `dokku`: `git remote add dokku dokku@{domain}:city-map` -- To Deploy: `git push dokku master` -- To deploy a branch other than master, alias it to master: `git push dokku {branchname}:master` - -## Uploading New Files to Digital Ocean Spaces - -1. Ask the client to send you the new files they want uploaded to the app. This can be over Slack, Box, Dropbox, etc. -2. Download the new files in a folder on your machine. - -3. Before uploading these files to Spaces, you need to install s3cmd on your machine. You can download the file here: https://s3tools.org/download -4. Once you have s3cmd downloaded, navigate (in your terminal) to the s3cmd folder you just downloaded -5. Run the command $ sudo python setup.py install -6. Then run $ s3cmd --configure - - Fill in the new config options: - -Access Key: access key (this will be given to you by another team member) -Secret Key: secret key (this will be given to you by another team member) -Default Region [US]: [press enter] -S3 Endpoint: nyc3.digitaloceanspaces.com -DNS-style bucket+hostname: %(bucket)s.nyc3.digitaloceanspaces.com -Encryption password: [press enter] -Path to GPG program: [press enter] -Use HTTPS protocol [Yes/No]: Yes -HTTP Proxy server name: [press enter] -Test access with supplied credentials? [Y/n] y - -//If you see this error about GPG program, type in "n" for Retry configuration - -ERROR: Test failed: GPG program not found -Retry configuration? [Y/n] n - -Save settings? [y/N] y -Configuration saved to '/Users/YourFile/.s3cfg' - -7. After setting up the config, navigate to the directory with the files you downloaded from the client, and run this command in the terminal: - - $ s3cmd --acl-public put ./* s3://nycdcp-dcm-alteration-maps --recursive - - // “--acl-public” allows anyone to view the file online - // “ ./* ” signifies a specific path to follow to locate the folder - // “s3://nycdcp-dcm-alteration-maps” is the folder on Spaces where you are adding the new files - // "--recursive" is necessary to upload a directory - -8. Make sure to have another team member add you to the Digital Ocean account. You will receive an email when you have been added. Create a new account from this email, and now you are able to view the files that have been uploaded to the Spaces folder. - - ## Contact us -You can find us on Twitter at [@nycplanninglabs](https://twitter.com/nycplanninglabs), or comment on issues and we'll follow up as soon as we can. If you'd like to send an email, use [labs_dl@planning.nyc.gov](mailto:labs_dl@planning.nyc.gov) +You can find us on Twitter at [@NYCPlanningTech](https://twitter.com/nycplanningtech), or comment on issues and we'll follow up as soon as we can. If you'd like to send an email, use [OpenSource_dl@planning.nyc.gov](mailto:opensource_dl@planning.nyc.gov) diff --git a/app/components/custom-controls.js b/app/components/custom-controls.js index 20642b10..575ad70e 100644 --- a/app/components/custom-controls.js +++ b/app/components/custom-controls.js @@ -14,14 +14,14 @@ export default class CustomControls extends Component { @argument boundsGeoJSON; datasets = [ - { tableName: 'citymap_citymap_v1', displayName: 'City Street Features' }, - { tableName: 'citymap_amendments_v3', displayName: 'City Map Amendments' }, - { tableName: 'citymap_streetcenterlines_v1', displayName: 'Street Centerlines' }, - { tableName: 'citymap_streetnamechanges_points_v0', displayName: 'Street Name Changes - Points' }, - { tableName: 'citymap_streetnamechanges_streets_v0', displayName: 'Street Name Changes - Streets' }, - { tableName: 'citymap_streetnamechanges_areas_v0', displayName: 'Street Name Changes - Areas' }, - { tableName: 'citymap_pierheadlines_v1', displayName: 'Pierhead and Bulkhead Lines' }, - { tableName: 'citymap_arterials_v0', displayName: 'Arterials' }, + { tableName: 'dcp_dcm', displayName: 'City Street Features', queryModifier: "feat_type NOT IN ('Pierhead_line', 'Bulkhead_line', 'Rail')" }, + { tableName: 'dcp_dcm_city_map_alterations', displayName: 'City Map Amendments' }, + { tableName: 'dcp_dcm_street_centerline', displayName: 'Street Centerlines' }, + { tableName: 'dcp_dcm_street_name_changes_points', displayName: 'Street Name Changes - Points' }, + { tableName: 'dcp_dcm_street_name_changes_lines', displayName: 'Street Name Changes - Streets' }, + { tableName: 'dcp_dcm_street_name_changes_areas', displayName: 'Street Name Changes - Areas' }, + { tableName: 'dcp_dcm', displayName: 'Pierhead and Bulkhead Lines', queryModifier: "feat_type IN ('Pierhead_line', 'Bulkhead_line')" }, + { tableName: 'dcp_dcm_arterials_major_streets', displayName: 'Arterials' }, ] @computed('boundsGeoJSON') diff --git a/app/components/map-popup-content.js b/app/components/map-popup-content.js index 00e85784..2555cc7d 100644 --- a/app/components/map-popup-content.js +++ b/app/components/map-popup-content.js @@ -95,6 +95,30 @@ export default class MapPopupContent extends Component { return streetNameChanges; } + @computed('features') + get sectionMapLink() { + const features = this.get('features'); + if (features === null) return features; + + const sectionMapLink = features + .filter(d => d.properties.type === 'streetsect') + .map((feature) => { + const { properties } = feature; + const { do_path, last_date, boro } = properties; + + return { + feature, + do_path, + boro, + boroname: boroLookup[boro], + last_date: moment(last_date).format('MMM D, YYYY'), + section_info: do_path === null ? null : do_path.split('.com/')[1], + }; + }); + + return sectionMapLink; + } + @argument features = []; diff --git a/app/controllers/application.js b/app/controllers/application.js index 76def697..82aeca6f 100644 --- a/app/controllers/application.js +++ b/app/controllers/application.js @@ -218,6 +218,8 @@ export default class ApplicationController extends ParachuteController { const amendmentsLayerDisabled = this.get('model').layerGroups.toArray().filter(layerGroup => layerGroup.get('visible')).find(layer => layer.id === 'amendments') === undefined; + const streetSectionLayerDisabled = this.get('model').layerGroups.toArray().filter(layerGroup => layerGroup.get('visible')).find(layer => layer.id === 'street-sections') === undefined; + // Open the popup and clear its content (defaults to showing spinner) this.set('popupFeatures', null); this.set('popupLocation', e.lngLat); @@ -225,9 +227,9 @@ export default class ApplicationController extends ParachuteController { // Query and set the popup content const { lng, lat } = e.lngLat; const SQL = ` - SELECT the_geom, 'alteration' AS type, altmappdf, status, effective, NULL AS bbl, NULL AS address - FROM citymap_amendments_v3 - WHERE (effective IS NOT NULL + SELECT the_geom, 'alteration' AS type, altmappdf, status, effect_dt AS effective, NULL AS bbl, NULL AS address, NULL::timestamp AS last_date, NULL AS do_path, NULL AS boro + FROM dcp_dcm_city_map_alterations + WHERE (effect_dt IS NOT NULL OR status = '13') AND ST_Intersects( the_geom, @@ -239,7 +241,7 @@ export default class ApplicationController extends ParachuteController { ) ) UNION ALL - SELECT the_geom, 'taxlot' AS type, NULL as altmappdf, NULL as status, NULL as effective, bbl, address + SELECT the_geom, 'taxlot' AS type, NULL AS altmappdf, NULL AS status, NULL AS effective, bbl, address, NULL::timestamp AS last_date, NULL AS do_path, NULL AS boro FROM dcp_mappluto WHERE ST_Intersects( the_geom, @@ -250,6 +252,18 @@ export default class ApplicationController extends ParachuteController { ),4326 ) ) + UNION ALL + SELECT the_geom, 'streetsect' AS type, NULL AS altmappdf, NULL AS status, NULL AS effective, NULL AS bbl, NULL AS address, last_date, do_path, boro + FROM dcp_final_section_map_index + WHERE ST_Intersects( + the_geom, + ST_SetSRID( + ST_MakePoint( + ${lng}, + ${lat} + ),4326 + ) + ) `; carto.SQL(SQL, 'geojson') @@ -271,8 +285,9 @@ export default class ApplicationController extends ParachuteController { let filteredFeatures = FC.features; if (citymapLayerDisabled) filteredFeatures = filteredFeatures.filter(feature => feature.properties.type !== 'taxlot'); - if (amendmentsLayerDisabled) filteredFeatures = filteredFeatures.filter(feature => (feature.properties.type === 'alteration' && feature.properties.effective === null) || feature.properties.type === 'taxlot'); - if (pendingAmendmentsLayerDisabled) filteredFeatures = filteredFeatures.filter(feature => (feature.properties.type === 'alteration' && feature.properties.effective !== null) || feature.properties.type === 'taxlot'); + if (amendmentsLayerDisabled) filteredFeatures = filteredFeatures.filter(feature => (feature.properties.type === 'alteration' && feature.properties.effective === null) || feature.properties.type === 'taxlot' || feature.properties.type === 'streetsect'); + if (pendingAmendmentsLayerDisabled) filteredFeatures = filteredFeatures.filter(feature => (feature.properties.type === 'alteration' && feature.properties.effective !== null) || feature.properties.type === 'taxlot' || feature.properties.type === 'streetsect'); + if (streetSectionLayerDisabled) filteredFeatures = filteredFeatures.filter(feature => (feature.properties.type !== 'streetsect')); this.set('popupFeatures', [...filteredFeatures, ...streetNameChanges]); }); diff --git a/app/routes/application.js b/app/routes/application.js index d059b36b..4400a878 100644 --- a/app/routes/application.js +++ b/app/routes/application.js @@ -23,6 +23,7 @@ export default class ApplicationRoute extends Route { { id: 'citymap', visible: true }, { id: 'street-centerlines', visible: true }, { id: 'pierhead-bulkhead-lines', visible: true }, + { id: 'street-sections', visible: false }, { id: 'amendments', visible: true }, { id: 'amendments-pending', visible: false }, { id: 'arterials', visible: false }, diff --git a/app/styles/layouts/_l-default.scss b/app/styles/layouts/_l-default.scss index f868336e..ca73020a 100644 --- a/app/styles/layouts/_l-default.scss +++ b/app/styles/layouts/_l-default.scss @@ -283,6 +283,13 @@ hr.small-margin { } } +.null-section-map{ + display: flex; + flex-wrap: wrap; + max-height: min-content; + max-width: 15rem; +} + // Multi-column lists .list-float-3 { @include clearfix; diff --git a/app/templates/application.hbs b/app/templates/application.hbs index c3ab71c9..bbeb6b65 100644 --- a/app/templates/application.hbs +++ b/app/templates/application.hbs @@ -261,6 +261,16 @@ {{/labs-ui/layer-group-toggle}} {{/lookup-layer-group}} + {{#lookup-layer-group for='street-sections' as |layerGroup|}} + {{#labs-ui/layer-group-toggle + label=layerGroup.model.legend.label + tooltip=layerGroup.model.legend.tooltip + active=layerGroup.model.visible + icon=layerGroup.model.legend.icon + }} + {{/labs-ui/layer-group-toggle}} + {{/lookup-layer-group}} + {{#lookup-layer-group for='amendments' as |layerGroup|}} {{#labs-ui/layer-group-toggle label=layerGroup.model.legend.label diff --git a/app/templates/components/custom-controls.hbs b/app/templates/components/custom-controls.hbs index 7671b7d1..84837e04 100644 --- a/app/templates/components/custom-controls.hbs +++ b/app/templates/components/custom-controls.hbs @@ -43,9 +43,18 @@
  • {{fa-icon 'download'}} {{dataset.displayName}}: - All (SHP) + {{#if dataset.queryModifier}} + All (SHP) + {{else}} + All (SHP) + {{/if}} | - In View (SHP) + {{#if dataset.queryModifier}} + In View (SHP) + {{else}} + In View (SHP) + {{/if}} +

  • {{/each}} diff --git a/app/templates/components/map-popup-content.hbs b/app/templates/components/map-popup-content.hbs index af734774..0cd9c604 100644 --- a/app/templates/components/map-popup-content.hbs +++ b/app/templates/components/map-popup-content.hbs @@ -33,6 +33,28 @@ {{/if}} + {{#if sectionMapLink}} +
    + + + {{/if}} + {{#if cleanLots}}