2023-06-10 20:11:12 +03:00
|
|
|
#!/usr/bin/env node
|
|
|
|
|
|
|
|
const turf = require("@turf/turf");
|
|
|
|
const fs = require("fs");
|
|
|
|
const YAML = require("yaml");
|
|
|
|
const _ = require("lodash");
|
|
|
|
const path = require("node:path");
|
2023-08-03 16:35:34 +03:00
|
|
|
const md5 = require("md5");
|
2023-06-10 20:11:12 +03:00
|
|
|
|
|
|
|
const yargs = require("yargs/yargs");
|
|
|
|
const { hideBin } = require("yargs/helpers");
|
|
|
|
const args = yargs(hideBin(process.argv)).argv;
|
|
|
|
|
|
|
|
const geofixConf = {
|
|
|
|
projectFolder: args.projectFolder,
|
|
|
|
layers: args.layers || path.join(args.projectFolder, "src", "layers.yaml"),
|
|
|
|
properties:
|
|
|
|
args.properties || path.join(args.projectFolder, "src", "properties.yaml"),
|
|
|
|
config: args.config || path.join(args.projectFolder, "src", "config.yaml"),
|
|
|
|
countries:
|
|
|
|
args.countries || path.join(args.projectFolder, "src", "countries"),
|
|
|
|
naturesObjects:
|
|
|
|
args.naturesObjects || path.join(args.projectFolder, "src", "nature"),
|
|
|
|
roads: args.roads || path.join(args.projectFolder, "src", "roads"),
|
|
|
|
output: args.output || path.join(args.projectFolder, "geo.geojson"),
|
|
|
|
};
|
|
|
|
|
|
|
|
let layers = YAML.parse(fs.readFileSync(geofixConf.layers, "utf-8"));
|
2023-08-03 16:35:34 +03:00
|
|
|
let countries_properties =
|
|
|
|
YAML.parse(fs.readFileSync(geofixConf.properties, "utf-8")) || {};
|
2023-06-10 20:11:12 +03:00
|
|
|
let config = YAML.parse(fs.readFileSync(geofixConf.config, "utf-8"));
|
|
|
|
|
|
|
|
let features = [];
|
|
|
|
|
2024-01-24 09:36:42 +03:00
|
|
|
for (let country of layers) {
|
2023-08-03 16:35:34 +03:00
|
|
|
let properties = countries_properties[country] || {};
|
2023-06-10 20:11:12 +03:00
|
|
|
|
|
|
|
let co_features = JSON.parse(
|
|
|
|
fs.readFileSync(
|
|
|
|
path.join(geofixConf.countries, `${country}.geojson`),
|
|
|
|
"utf-8"
|
|
|
|
)
|
|
|
|
);
|
|
|
|
|
2023-11-26 15:56:49 +03:00
|
|
|
co_features.features = co_features.features.flatMap((val) => {
|
|
|
|
if (val?.geometry?.type === "MultiPolygon") {
|
|
|
|
return val.geometry.coordinates.map((coordinateSet) => {
|
|
|
|
return {
|
|
|
|
...val,
|
|
|
|
geometry: {
|
|
|
|
type: "Polygon",
|
|
|
|
coordinates: coordinateSet,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
return val;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2023-07-05 15:46:25 +03:00
|
|
|
co_features.features = co_features.features.map((val) => {
|
2023-11-26 15:56:49 +03:00
|
|
|
if (val?.geometry?.type.endsWith("Polygon")) {
|
2023-07-05 15:46:25 +03:00
|
|
|
val.properties = {};
|
|
|
|
}
|
2023-07-05 15:54:40 +03:00
|
|
|
|
|
|
|
return val;
|
2023-07-05 15:46:25 +03:00
|
|
|
});
|
|
|
|
|
2023-06-10 20:11:12 +03:00
|
|
|
fs.writeFileSync(
|
|
|
|
path.join(geofixConf.countries, `${country}.geojson`),
|
|
|
|
JSON.stringify(co_features, null, " ")
|
|
|
|
);
|
|
|
|
|
2023-08-03 16:35:34 +03:00
|
|
|
if (!properties.fill) {
|
|
|
|
properties.fill = `#${md5(country).substring(0, 6)}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!properties.stroke) {
|
|
|
|
properties.stroke = `#${md5(country).substring(6, 12)}`;
|
|
|
|
}
|
|
|
|
|
2023-06-10 20:11:12 +03:00
|
|
|
co_features = co_features.features;
|
|
|
|
|
2023-08-03 16:35:34 +03:00
|
|
|
features.unshift(
|
2023-06-10 20:11:12 +03:00
|
|
|
...co_features.map((val) => {
|
|
|
|
if (val.geometry.type == "Polygon") {
|
2023-11-24 14:30:31 +03:00
|
|
|
val.properties = properties || {};
|
2023-06-10 20:11:12 +03:00
|
|
|
val.properties.name = country;
|
|
|
|
}
|
|
|
|
|
|
|
|
return val;
|
2023-08-03 16:35:34 +03:00
|
|
|
})
|
|
|
|
);
|
2023-06-10 20:11:12 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
let geo = {
|
|
|
|
type: "FeatureCollection",
|
2023-08-03 16:35:34 +03:00
|
|
|
features,
|
2023-06-10 20:11:12 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
geo.features = geo.features.filter((v) => v.properties.name);
|
|
|
|
|
|
|
|
console.time("Total");
|
|
|
|
|
|
|
|
console.log("Dissolve");
|
|
|
|
console.time("Dissolve");
|
|
|
|
let nonPoly = geo.features.filter((v) => !v.geometry.type.endsWith("Polygon"));
|
|
|
|
|
|
|
|
nonPoly = nonPoly.map((v) => {
|
|
|
|
if (v.properties.type === "landmark") {
|
|
|
|
v.properties.type = "landmark-0";
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!v.properties.type) {
|
|
|
|
v.properties.type = "city";
|
|
|
|
}
|
|
|
|
|
|
|
|
return v;
|
|
|
|
});
|
|
|
|
|
|
|
|
let polygons = geo.features.filter((v) => v.geometry.type.endsWith("Polygon"));
|
|
|
|
let props = {};
|
|
|
|
|
|
|
|
for (let feature of polygons) {
|
|
|
|
if (props[feature.properties.name]) continue;
|
|
|
|
props[feature.properties.name] = {
|
|
|
|
stroke: feature.properties.stroke,
|
|
|
|
fill: feature.properties.fill,
|
|
|
|
type: feature.properties.type,
|
|
|
|
tags: feature.properties.tags,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
let dissolved = turf.dissolve(turf.featureCollection(polygons), {
|
|
|
|
propertyName: "name",
|
|
|
|
});
|
|
|
|
|
|
|
|
dissolved.features = dissolved.features.map((v) => {
|
|
|
|
v.properties = {
|
|
|
|
name: v.properties.name,
|
|
|
|
fill: props[v.properties.name].fill,
|
|
|
|
stroke: props[v.properties.name].stroke,
|
|
|
|
type: props[v.properties.name].type,
|
|
|
|
tags: props[v.properties.name].tags,
|
|
|
|
};
|
|
|
|
return v;
|
|
|
|
});
|
|
|
|
geo.features = dissolved.features.concat(nonPoly);
|
|
|
|
console.timeEnd("Dissolve");
|
|
|
|
console.log();
|
|
|
|
|
|
|
|
console.log("Polygons to MultiPolygons");
|
|
|
|
console.time("Polygons to MultiPolygons");
|
|
|
|
|
|
|
|
polygons = geo.features.filter((v) => v.geometry.type.endsWith("Polygon"));
|
|
|
|
nonPoly = geo.features.filter((v) => !v.geometry.type.endsWith("Polygon"));
|
|
|
|
|
|
|
|
let countries = {};
|
|
|
|
|
|
|
|
for (let somePolygon of polygons) {
|
|
|
|
if (!countries[somePolygon.properties.name]) {
|
|
|
|
countries[somePolygon.properties.name] = {
|
|
|
|
type: "Feature",
|
|
|
|
properties: somePolygon.properties,
|
|
|
|
geometry: {
|
|
|
|
type: "MultiPolygon",
|
|
|
|
coordinates: [],
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (somePolygon.geometry.type === "MultiPolygon") {
|
|
|
|
countries[somePolygon.properties.name].geometry.coordinates = countries[
|
|
|
|
somePolygon.properties.name
|
|
|
|
].concat(somePolygon.geometry.coordinates);
|
|
|
|
} else if (somePolygon.geometry.type === "Polygon") {
|
|
|
|
countries[somePolygon.properties.name].geometry.coordinates.push(
|
|
|
|
somePolygon.geometry.coordinates
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let multiPolygons = Object.values(countries);
|
|
|
|
|
|
|
|
geo.features = multiPolygons.concat(nonPoly);
|
|
|
|
|
|
|
|
console.timeEnd("Polygons to MultiPolygons");
|
|
|
|
console.log();
|
|
|
|
|
|
|
|
console.log("Difference");
|
|
|
|
console.time("Difference");
|
|
|
|
for (let g = 0; g < geo.features.length; g++) {
|
|
|
|
for (let i = 0; i < geo.features.length; i++) {
|
|
|
|
try {
|
|
|
|
if (
|
|
|
|
geo.features[g] === geo.features[i] ||
|
|
|
|
geo.features[i]?.properties.type === "sand" ||
|
|
|
|
geo.features[i]?.properties.type === "water" ||
|
|
|
|
geo.features[i]?.properties.type === "grass" ||
|
|
|
|
geo.features[g]?.properties.type === "sand" ||
|
|
|
|
geo.features[g]?.properties.type === "water" ||
|
|
|
|
geo.features[g]?.properties.type === "grass"
|
|
|
|
) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (
|
|
|
|
(geo.features[g]?.geometry.type === "Polygon" ||
|
|
|
|
geo.features[g]?.geometry.type === "MultiPolygon") &&
|
|
|
|
(geo.features[i]?.geometry.type === "Polygon" ||
|
|
|
|
geo.features[i]?.geometry.type === "MultiPolygon")
|
|
|
|
) {
|
|
|
|
let p1 =
|
|
|
|
geo.features[g]?.geometry.type === "MultiPolygon"
|
|
|
|
? turf.multiPolygon(
|
|
|
|
geo.features[g].geometry.coordinates,
|
|
|
|
geo.features[g].properties
|
|
|
|
)
|
|
|
|
: turf.polygon(
|
|
|
|
geo.features[g].geometry.coordinates,
|
|
|
|
geo.features[g].properties
|
|
|
|
);
|
|
|
|
let p2 =
|
|
|
|
geo.features[i]?.geometry.type === "MultiPolygon"
|
|
|
|
? turf.multiPolygon(
|
|
|
|
geo.features[i].geometry.coordinates,
|
|
|
|
geo.features[i].properties
|
|
|
|
)
|
|
|
|
: turf.polygon(
|
|
|
|
geo.features[i].geometry.coordinates,
|
|
|
|
geo.features[i].properties
|
|
|
|
);
|
|
|
|
|
|
|
|
let diff = turf.difference(p1, p2);
|
|
|
|
geo.features[g] = diff ? diff : geo.features[g];
|
|
|
|
} else continue;
|
|
|
|
} catch (e) {
|
|
|
|
console.log("Error, skip \n", e, "\n");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
console.timeEnd("Difference");
|
|
|
|
console.log();
|
|
|
|
|
|
|
|
console.log("Add Map Components");
|
|
|
|
console.time("Add Map Components");
|
|
|
|
|
|
|
|
let sand = JSON.parse(
|
|
|
|
fs.readFileSync(path.join(geofixConf.naturesObjects, "sand.geojson"), "utf-8")
|
|
|
|
).features;
|
|
|
|
|
|
|
|
let water = JSON.parse(
|
|
|
|
fs.readFileSync(
|
|
|
|
path.join(geofixConf.naturesObjects, "water.geojson"),
|
|
|
|
"utf-8"
|
|
|
|
)
|
|
|
|
).features;
|
|
|
|
|
|
|
|
let grass = JSON.parse(
|
|
|
|
fs.readFileSync(
|
|
|
|
path.join(geofixConf.naturesObjects, "grass.geojson"),
|
|
|
|
"utf-8"
|
|
|
|
)
|
|
|
|
).features;
|
|
|
|
|
|
|
|
let white_road = JSON.parse(
|
|
|
|
fs.readFileSync(path.join(geofixConf.roads, "white.geojson"), "utf-8")
|
|
|
|
).features;
|
|
|
|
|
|
|
|
let orange_road = JSON.parse(
|
|
|
|
fs.readFileSync(path.join(geofixConf.roads, "orange.geojson"), "utf-8")
|
|
|
|
).features;
|
|
|
|
|
|
|
|
let yellow_road = JSON.parse(
|
|
|
|
fs.readFileSync(path.join(geofixConf.roads, "yellow.geojson"), "utf-8")
|
|
|
|
).features;
|
|
|
|
|
|
|
|
let road_sizes = JSON.parse(
|
|
|
|
fs.readFileSync(path.join(geofixConf.roads, "sizes.json"), "utf-8")
|
|
|
|
);
|
|
|
|
|
|
|
|
let map_comps = [
|
|
|
|
...water.map((val) => {
|
|
|
|
val.properties.type = "water";
|
|
|
|
val.properties.fill = "#75cff0";
|
|
|
|
val.properties.stroke = "#75cff0";
|
|
|
|
val.properties["fill-opacity"] = 1;
|
|
|
|
return val;
|
|
|
|
}),
|
|
|
|
...sand.map((val) => {
|
|
|
|
val.properties.type = "sand";
|
|
|
|
val.properties.fill = "#efe9e1";
|
|
|
|
val.properties.stroke = "#efe9e1";
|
|
|
|
val.properties["fill-opacity"] = 1;
|
|
|
|
return val;
|
|
|
|
}),
|
|
|
|
...grass.map((val) => {
|
|
|
|
val.properties.type = "grass";
|
|
|
|
val.properties.fill = "#d1e6be";
|
|
|
|
val.properties.stroke = "#d1e6be";
|
|
|
|
val.properties["fill-opacity"] = 1;
|
|
|
|
return val;
|
|
|
|
}),
|
|
|
|
...white_road.map((val) => {
|
|
|
|
let total = turf.buffer(
|
|
|
|
val,
|
|
|
|
road_sizes[val.properties.type] || road_sizes["middle"]
|
|
|
|
);
|
|
|
|
total.properties.type = "white_road";
|
|
|
|
|
|
|
|
val.properties.fill = "#fff";
|
|
|
|
val.properties.stroke = "#fff";
|
|
|
|
val.properties["fill-opacity"] = 1;
|
|
|
|
|
|
|
|
return total;
|
|
|
|
}),
|
|
|
|
...yellow_road.map((val) => {
|
|
|
|
let total = turf.buffer(
|
|
|
|
val,
|
|
|
|
road_sizes[val.properties.type] || road_sizes["middle"]
|
|
|
|
);
|
|
|
|
total.properties.type = "yellow_road";
|
|
|
|
val.properties.fill = "#ffc107";
|
|
|
|
val.properties.stroke = "#ffc107";
|
|
|
|
val.properties["fill-opacity"] = 1;
|
|
|
|
return total;
|
|
|
|
}),
|
|
|
|
...orange_road.map((val) => {
|
|
|
|
let total = turf.buffer(
|
|
|
|
val,
|
|
|
|
road_sizes[val.properties.type] || road_sizes["big"]
|
|
|
|
);
|
|
|
|
total.properties.type = "orange_road";
|
|
|
|
val.properties.fill = "#fd7e14";
|
|
|
|
val.properties.stroke = "#fd7e14";
|
|
|
|
val.properties["fill-opacity"] = 1;
|
|
|
|
return total;
|
|
|
|
}),
|
|
|
|
];
|
|
|
|
|
|
|
|
props = {};
|
|
|
|
|
|
|
|
for (let feature of map_comps) {
|
|
|
|
if (props[feature.properties.type]) continue;
|
|
|
|
props[feature.properties.type] = {
|
|
|
|
stroke: feature.properties.stroke,
|
|
|
|
fill: feature.properties.fill,
|
|
|
|
type: feature.properties.type,
|
|
|
|
tags: feature.properties.tags,
|
|
|
|
"fill-opacity": feature.properties["fill-opacity"],
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
map_comps = turf.dissolve(turf.featureCollection(map_comps), {
|
|
|
|
propertyName: "type",
|
|
|
|
});
|
|
|
|
|
|
|
|
map_comps.features = map_comps.features.map((v) => {
|
|
|
|
v.properties = {
|
|
|
|
fill: props[v.properties.type].fill,
|
|
|
|
stroke: props[v.properties.type].stroke,
|
|
|
|
type: props[v.properties.type].type,
|
|
|
|
tags: props[v.properties.type].tags,
|
|
|
|
"fill-opacity": props[v.properties.type]["fill-opacity"],
|
|
|
|
};
|
|
|
|
return v;
|
|
|
|
});
|
|
|
|
|
|
|
|
geo.features = [...map_comps.features, ...geo.features];
|
|
|
|
console.timeEnd("Add Map Components");
|
|
|
|
console.log();
|
|
|
|
|
|
|
|
if (config?.tags) {
|
|
|
|
console.log("Filter countries by tags");
|
|
|
|
console.time("Filter countries by tags");
|
|
|
|
|
|
|
|
geo.features = geo.features.filter((val) => {
|
|
|
|
if (_.intersection(config.tags, val.properties.tags).length === 0)
|
|
|
|
return false;
|
|
|
|
// else if (config?.cities == false && val.geometry.type === "Point")
|
|
|
|
// return false;
|
|
|
|
else return true;
|
|
|
|
});
|
|
|
|
|
|
|
|
console.timeEnd("Filter countries by tag");
|
|
|
|
console.log();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (config?.reProperty) {
|
|
|
|
console.log("replace Properties");
|
|
|
|
console.time("replace Properties");
|
|
|
|
|
|
|
|
geo.features = geo.features.map((val) => {
|
|
|
|
val.properties = config.reProperty;
|
|
|
|
return val;
|
|
|
|
});
|
|
|
|
|
|
|
|
console.timeEnd("replace Properties");
|
|
|
|
console.log();
|
|
|
|
}
|
|
|
|
|
2023-07-05 16:06:16 +03:00
|
|
|
console.log("Set new ids and area");
|
|
|
|
console.time("Set new ids and area");
|
2023-06-10 20:11:12 +03:00
|
|
|
|
|
|
|
let id = 0;
|
|
|
|
geo.features = geo.features.map((val) => {
|
|
|
|
val.id = id++;
|
2023-07-05 16:20:22 +03:00
|
|
|
if (val.geometry.type.endsWith("Polygon") && !val.properties.type) {
|
2023-07-05 16:09:05 +03:00
|
|
|
val.properties.area = (turf.area(val) / 1000000)
|
|
|
|
.toFixed(2)
|
|
|
|
.replace(/(\d)(?=(\d\d\d)+([^\d]|$))/g, "$1 ");
|
|
|
|
}
|
2023-06-10 20:11:12 +03:00
|
|
|
return val;
|
|
|
|
});
|
|
|
|
|
2023-07-05 16:06:16 +03:00
|
|
|
console.timeEnd("Set new ids and area");
|
2023-06-10 20:11:12 +03:00
|
|
|
|
|
|
|
fs.writeFileSync(geofixConf.output, JSON.stringify(geo, null, " "));
|
|
|
|
console.timeEnd("Total");
|