diff --git a/CHANGELOG.md b/CHANGELOG.md index 547f58d1b..af023b5e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed ### Fixed +## [1.12.1] - 2021-10-29 +### Fixed +- Fix import for regions that have a geounit with 1 identical geometry nested under it [#1053](https://github.com/PublicMapping/districtbuilder/pull/1053) +- Fix simplification for geojson with invalid geometries [#1054](https://github.com/PublicMapping/districtbuilder/pull/1054) +- Fix population deviation tooltip [#1055](https://github.com/PublicMapping/districtbuilder/pull/1055) + ## [1.12.0] - 2021-10-25 ### Changed - Support negative/adjusted population values [#1043](https://github.com/PublicMapping/districtbuilder/pull/1043) @@ -355,7 +361,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release. -[unreleased]: https://github.com/publicmapping/districtbuilder/compare/1.12.0...HEAD +[unreleased]: https://github.com/publicmapping/districtbuilder/compare/1.12.1...HEAD +[1.12.1]: https://github.com/publicmapping/districtbuilder/compare/1.12.0...1.12.1 [1.12.0]: https://github.com/publicmapping/districtbuilder/compare/1.11.0...1.12.0 [1.11.0]: https://github.com/publicmapping/districtbuilder/compare/1.10.1...1.11.0 [1.10.1]: https://github.com/publicmapping/districtbuilder/compare/1.10.0...1.10.1 diff --git a/src/client/components/ProjectSidebar.tsx b/src/client/components/ProjectSidebar.tsx index d06333512..d4f4b9a5d 100644 --- a/src/client/components/ProjectSidebar.tsx +++ b/src/client/components/ProjectSidebar.tsx @@ -670,7 +670,7 @@ const SidebarRow = memo(
Add{" "} {Math.floor( - Math.abs(intermediateDeviation) + popDeviationThreshold + Math.abs(intermediateDeviation) - popDeviationThreshold ).toLocaleString()}{" "} people to this district to meet the {popDeviation}% population deviation tolerance diff --git a/src/server/global.d.ts b/src/server/global.d.ts index bfd84040b..0dd8bb3be 100644 --- a/src/server/global.d.ts +++ b/src/server/global.d.ts @@ -14,3 +14,9 @@ declare module "geojson2shp" { options?: ConvertOptions ): Promise; } + +declare module "simplify-geojson" { + import { GeoJSON } from "geojson"; + + export function simplify(feature: GeoJSON, tolerance?: number): void; +} diff --git a/src/server/migrations/1635458920929-add_postgis.ts b/src/server/migrations/1635458920929-add_postgis.ts new file mode 100644 index 000000000..1bde8dfc5 --- /dev/null +++ b/src/server/migrations/1635458920929-add_postgis.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class addPostgis1635458920929 implements MigrationInterface { + name = "addPostgis1635458920929"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS "postgis"`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP EXTENSION IF EXISTS "postgis"`); + } +} diff --git a/src/server/package.json b/src/server/package.json index bccd322c1..4f51416d4 100644 --- a/src/server/package.json +++ b/src/server/package.json @@ -42,7 +42,6 @@ "@turf/bbox": "^6.5.0", "@turf/length": "6.0.2", "@turf/polygon-to-line": "6.0.3", - "@turf/simplify": "^6.5.0", "aws-sdk": "2.616.0", "base64url": "3.0.1", "bcrypt": "5.0.0", diff --git a/src/server/src/districts/entities/geo-unit-topology.entity.ts b/src/server/src/districts/entities/geo-unit-topology.entity.ts index ae571a85a..c367c80c9 100644 --- a/src/server/src/districts/entities/geo-unit-topology.entity.ts +++ b/src/server/src/districts/entities/geo-unit-topology.entity.ts @@ -300,7 +300,9 @@ export class GeoUnitTopology { // Keep recursing into the hierarchy until we reach the end const results = mapToDefinition(hierarchyNumOrArray); // Simplify if possible - return results.every(item => item === results[0]) ? results[0] : results; + return results.length !== 1 && results.every(item => item === results[0]) + ? results[0] + : results; } }); diff --git a/src/server/src/projects/services/projects.service.ts b/src/server/src/projects/services/projects.service.ts index 33bb5031e..2c21b0061 100644 --- a/src/server/src/projects/services/projects.service.ts +++ b/src/server/src/projects/services/projects.service.ts @@ -1,11 +1,7 @@ -import { Injectable, Logger } from "@nestjs/common"; +import { Injectable } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { TypeOrmCrudService } from "@nestjsx/crud-typeorm"; -import simplify from "@turf/simplify"; -import bbox from "@turf/bbox"; -import { BBox } from "@turf/helpers"; import { Repository, SelectQueryBuilder, DeepPartial } from "typeorm"; -import _ from "lodash"; import { Project } from "../entities/project.entity"; import { ProjectVisibility } from "../../../../shared/constants"; @@ -19,8 +15,6 @@ type AllProjectsOptions = IPaginationOptions & { @Injectable() export class ProjectsService extends TypeOrmCrudService { - private readonly logger = new Logger(ProjectsService.name); - constructor(@InjectRepository(Project) repo: Repository) { super(repo); } @@ -31,62 +25,47 @@ export class ProjectsService extends TypeOrmCrudService { } getProjectsBase(): SelectQueryBuilder { - return this.repo - .createQueryBuilder("project") - .innerJoin("project.regionConfig", "regionConfig") - .innerJoin("project.user", "user") - .leftJoin("project.chamber", "chamber") - .select([ - "project.id", - "project.name", - "project.numberOfDistricts", - "project.updatedDt", - "project.createdDt", - "project.districts", - "regionConfig.name", - "regionConfig.id", - "user.id", - "user.name" - ]) - .orderBy("project.updatedDt", "DESC"); - } - - computeBBoxArea(project: Project): number { - const box: BBox = bbox(project.districts); - return (box[2] - box[0]) * (box[3] - box[1]); - } - - // We only use the districts column for displaying a mini-map outside of the main Project Screen - // so we can simplify the geometries to save on size and improve performance - async simplifyDistricts(page: Promise>): Promise> { - const projects = await page; - projects.items.forEach(project => { - const boxArea = this.computeBBoxArea(project); - project.districts && - project.districts.features.forEach(districtFeature => { - // Some very small holes may collapse to a single point during the merge operation, - // and generate invalid polygons that cause simplify to fail - //eslint-disable-next-line functional/immutable-data - districtFeature.geometry.coordinates = districtFeature.geometry.coordinates - .map(polygonCoords => - polygonCoords.flatMap(ringCoords => { - if (ringCoords.every(coord => _.isEqual(coord, ringCoords[0]))) { - return []; - } - return [ringCoords]; - }) + return ( + this.repo + .createQueryBuilder("project") + .innerJoin("project.regionConfig", "regionConfig") + .innerJoin("project.user", "user") + .leftJoin("project.chamber", "chamber") + .select([ + "project.id", + "project.name", + "project.numberOfDistricts", + "project.updatedDt", + "project.createdDt", + "project.districts", + "regionConfig.name", + "regionConfig.id", + "user.id", + "user.name" + ]) + // Replace the districts column with a simplified one to save on response size + // + // Note that we're doing a bit of a trick here to replace the contents of the districts column, + // we need to select it above, and then give an alias here that will override that selection + .addSelect( + `CASE + WHEN districts IS NULL THEN NULL + ELSE JSON_BUILD_OBJECT( + 'type', 'FeatureCollection', + 'features', ARRAY( + SELECT JSON_BUILD_OBJECT( + 'type', 'Feature', + 'properties', feature->'properties', + 'geometry', ST_AsGeoJSON(ST_Simplify(ST_GeomFromGeoJSON(feature->'geometry'), 0.001))::json + ) + FROM jsonb_array_elements(districts->'features') feature + ) ) - .filter(polygonCoords => polygonCoords.length > 0); - try { - simplify(districtFeature, { mutate: true, tolerance: boxArea > 1 ? 0.005 : 0.001 }); - } catch (e) { - this.logger.debug( - `Could not simplify district ${districtFeature.id} for project ${project.id}: ${e}` - ); - } - }); - }); - return projects; + END`, + "project_districts" + ) + .orderBy("project.updatedDt", "DESC") + ); } async findAllPublishedProjectsPaginated( @@ -110,7 +89,7 @@ export class ProjectsService extends TypeOrmCrudService { ? builderWithFilter.andWhere("regionConfig.regionCode = :region", { region: options.region }) : builderWithFilter; - return this.simplifyDistricts(paginate(builderWithRegion, options)); + return paginate(builderWithRegion, options); } async findAllUserProjectsPaginated( @@ -122,6 +101,6 @@ export class ProjectsService extends TypeOrmCrudService { { userId } ); - return this.simplifyDistricts(paginate(builder, options)); + return paginate(builder, options); } } diff --git a/src/server/yarn.lock b/src/server/yarn.lock index 3fad30de3..c9f6adaf3 100644 --- a/src/server/yarn.lock +++ b/src/server/yarn.lock @@ -580,21 +580,6 @@ "@turf/helpers" "^6.5.0" "@turf/meta" "^6.5.0" -"@turf/clean-coords@^6.5.0": - version "6.5.0" - resolved "https://registry.yarnpkg.com/@turf/clean-coords/-/clean-coords-6.5.0.tgz#6690adf764ec4b649710a8a20dab7005efbea53f" - integrity sha512-EMX7gyZz0WTH/ET7xV8MyrExywfm9qUi0/MY89yNffzGIEHuFfqwhcCqZ8O00rZIPZHUTxpmsxQSTfzJJA1CPw== - dependencies: - "@turf/helpers" "^6.5.0" - "@turf/invariant" "^6.5.0" - -"@turf/clone@^6.5.0": - version "6.5.0" - resolved "https://registry.yarnpkg.com/@turf/clone/-/clone-6.5.0.tgz#895860573881ae10a02dfff95f274388b1cda51a" - integrity sha512-mzVtTFj/QycXOn6ig+annKrM6ZlimreKYz6f/GSERytOpgzodbQyOgkfwru100O1KQhhjSudKK4DsQ0oyi9cTw== - dependencies: - "@turf/helpers" "^6.5.0" - "@turf/distance@6.x": version "6.0.1" resolved "https://registry.yarnpkg.com/@turf/distance/-/distance-6.0.1.tgz#0761f28784286e7865a427c4e7e3593569c2dea8" @@ -620,13 +605,6 @@ dependencies: "@turf/helpers" "6.x" -"@turf/invariant@^6.5.0": - version "6.5.0" - resolved "https://registry.yarnpkg.com/@turf/invariant/-/invariant-6.5.0.tgz#970afc988023e39c7ccab2341bd06979ddc7463f" - integrity sha512-Wv8PRNCtPD31UVbdJE/KVAWKe7l6US+lJItRR/HOEW3eh+U/JwRCSUl/KZ7bmjM/C+zLNoreM2TU6OoLACs4eg== - dependencies: - "@turf/helpers" "^6.5.0" - "@turf/length@6.0.2": version "6.0.2" resolved "https://registry.yarnpkg.com/@turf/length/-/length-6.0.2.tgz#22d91a6d0174e862a3614865613f1aceb1162dac" @@ -658,16 +636,6 @@ "@turf/helpers" "6.x" "@turf/invariant" "6.x" -"@turf/simplify@^6.5.0": - version "6.5.0" - resolved "https://registry.yarnpkg.com/@turf/simplify/-/simplify-6.5.0.tgz#ec435460bde0985b781618b05d97146c32c8bc16" - integrity sha512-USas3QqffPHUY184dwQdP8qsvcVH/PWBYdXY5am7YTBACaQOMAlf6AKJs9FT8jiO6fQpxfgxuEtwmox+pBtlOg== - dependencies: - "@turf/clean-coords" "^6.5.0" - "@turf/clone" "^6.5.0" - "@turf/helpers" "^6.5.0" - "@turf/meta" "^6.5.0" - "@types/anymatch@*": version "1.3.1" resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a"