Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes a bug with how we load forecast and fire zones from shapefile #1463

Merged
merged 4 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/actions/populate-database/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ runs:
uses: actions/cache@v4
id: db-cache
with:
key: drupal-database-${{ hashFiles('web/config/**/*.yml', 'web/scs-export/**/*', 'web/modules/weather_i18n/translations/*.po') }}
key: drupal-database-${{ hashFiles('web/config/**/*.yml', 'web/scs-export/**/*', 'web/modules/weather_i18n/translations/*.po', 'spatial/**/*.js') }}
path: weathergov.sql

- name: setup image cacheing
Expand Down
2 changes: 1 addition & 1 deletion .github/actions/setup-site/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ runs:
uses: actions/cache@v4
id: db-cache
with:
key: drupal-database-${{ hashFiles('web/config/**/*.yml', 'web/scs-export/**/*', 'web/modules/weather_i18n/translations/*.po') }}
key: drupal-database-${{ hashFiles('web/config/**/*.yml', 'web/scs-export/**/*', 'web/modules/weather_i18n/translations/*.po', 'spatial/**/*.js') }}
path: weathergov.sql

- name: start the site
Expand Down
4 changes: 3 additions & 1 deletion spatial-data/lib/prep.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,7 @@ module.exports.downloadAndUnzip = async (url) => {

module.exports.unzip = async (path) => {
console.log(` [${path}] decompressing...`);
await exec(`unzip -u ${path}`);

// Use -o to overwrite existing files.
await exec(`unzip -o -u ${path}`);
};
65 changes: 44 additions & 21 deletions spatial-data/sources/zones.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
const fs = require("node:fs/promises");
const shapefile = require("shapefile");

const { dropIndexIfExists, openDatabase } = require("../lib/db.js");

const metadata = {
table: "weathergov_geo_zones",
version: 1,
version: 2,
};

module.exports = async () => {
Expand All @@ -24,6 +25,13 @@ module.exports = async () => {
await dropIndexIfExists(db, "zones_spatial_idx", metadata.table);
await db.query(`TRUNCATE TABLE ${metadata.table}`);

// Version 2: Change the shape column into a collection rather than a single
// multipolygon. This allows us to capture all of the polygons for a zone as
// a collection rather than trying to collect or union them into one entity.
await db.query(
`ALTER TABLE ${metadata.table} MODIFY shape GEOMETRYCOLLECTION`,
);

const found = new Map();

const processFile = async (filename, zoneType) => {
Expand All @@ -39,30 +47,23 @@ module.exports = async () => {
geometry,
} = value;

if (geometry.type === "Polygon") {
geometry.type = "MultiPolygon";
geometry.coordinates = [geometry.coordinates];
}
// These shapefiles are in NAD83, whose SRID is 4269.
geometry.crs = { type: "name", properties: { name: "EPSG:4269" } };

const id = `https://api.weather.gov/zones/${zoneType}/${state}Z${zone}`;

// Some of the zones are duplicated. Dunno why. Don't put them in twice,
// the database will scream.
// Some of the zones are represented by multiple polygons. To handle that,
// we'll gather a list of all polygons and insert them into the database
// as a geometry collection.
if (!found.has(id)) {
found.set(id, { state, zone, zoneType, filename, geometry });

await db.query(
`INSERT INTO weathergov_geo_zones
(id, state, shape)
VALUES(
'${id}',
'${state}',
ST_GeomFromGeoJSON('${JSON.stringify(geometry)}')
)`,
);
found.set(id, {
state,
zone,
zoneType,
filename,
geometry: [geometry],
});
} else {
found.get(id).geometry.push(geometry);
}

return file.read().then(getSqlForShape);
};

Expand All @@ -72,6 +73,28 @@ module.exports = async () => {
await processFile(`./z_05mr24.shp`, "forecast");
await processFile(`./fz05mr24.shp`, "fire");

// Our map now contains entries for every zone. Iterate over that to insert
// them into the database.
for await (const [id, { state, geometry }] of found) {
const featureCollection = {
type: "FeatureCollection",
features: geometry,
// Shapefiles are in NAD83, whose SRID is 4269. Set that at the collection
// level so that it automatically applies to all contained shapes.
crs: { type: "name", properties: { name: "EPSG:4269" } },
};

await db.query(
`INSERT INTO ${metadata.table}
(id, state, shape)
VALUES(
'${id}',
'${state}',
ST_GeomFromGeoJSON('${JSON.stringify(featureCollection)}')
)`,
);
}

db.end();
};

Expand Down
14 changes: 11 additions & 3 deletions web/modules/weather_data/src/Service/AlertUtility.php
Original file line number Diff line number Diff line change
Expand Up @@ -464,9 +464,17 @@ public static function getGeometryAsJSON($alert, $dataLayer)

$polygon = json_decode($polygon->shape);

$polygon->coordinates = SpatialUtility::swapLatLon(
$polygon->coordinates,
);
if ($polygon->type == "GeometryCollection") {
foreach ($polygon->geometries as $innerPolygon) {
$innerPolygon->coordinates = SpatialUtility::swapLatLon(
$innerPolygon->coordinates,
);
}
} else {
$polygon->coordinates = SpatialUtility::swapLatLon(
$polygon->coordinates,
);
}
}

return $polygon;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,20 +88,29 @@ final class AlertUtilityGeometryTest extends TestCase
(object) ["shape" => "union 1,2,3"],
],

// Return the simplified shape
// Return the simplified shape as a geometry collection, so we
// can also test that those points are flipped properly.
[
"SELECT ST_ASGEOJSON(
ST_SIMPLIFY(
ST_GEOMFROMTEXT('union 1,2,3'),
0.003
)
) as shape",
(object) ["shape" => '{"coordinates":[[0,1],[2,3],[4,5]]}'],
(object) [
"shape" =>
'{"type":"GeometryCollection","geometries":[{"coordinates":[[0,1],[2,3],[4,5]]}]}',
],
],
]),
);

$expected = (object) ["coordinates" => [[1, 0], [3, 2], [5, 4]]];
$expected = (object) [
"type" => "GeometryCollection",
"geometries" => [
(object) ["coordinates" => [[1, 0], [3, 2], [5, 4]]],
],
];

$actual = AlertUtility::getGeometryAsJSON($alert, $this->dataLayer);
$this->assertEquals($expected, $actual);
Expand Down Expand Up @@ -176,12 +185,18 @@ final class AlertUtilityGeometryTest extends TestCase
0.003
)
) as shape",
(object) ["shape" => '{"coordinates":[[0,1],[2,3],[4,5]]}'],
(object) [
"shape" =>
'{"type":"Polygon","coordinates":[[0,1],[2,3],[4,5]]}',
],
],
]),
);

$expected = (object) ["coordinates" => [[1, 0], [3, 2], [5, 4]]];
$expected = (object) [
"type" => "Polygon",
"coordinates" => [[1, 0], [3, 2], [5, 4]],
];

$actual = AlertUtility::getGeometryAsJSON($alert, $this->dataLayer);
$this->assertEquals($expected, $actual);
Expand Down
Loading