homepage/garden-data-gen/source/plant_data.d

273 lines
9.5 KiB
D

module plant_data;
import dom_utils;
import std.stdio;
import std.array;
import std.algorithm;
import std.path;
import std.file;
import std.datetime;
import std.typecons;
import dxml.dom;
struct Species {
string id;
string name;
string scientificName;
string description;
string referenceLink;
}
struct Plant {
string speciesScientificName;
string identifier;
uint generation;
string plantingInfo;
string description;
}
string speciesId(string scientificName) {
import std.string;
import std.regex;
return scientificName
.replaceAll(ctRegex!(`\s+`), "-")
.replaceAll(ctRegex!(`ñ`), "n")
.replaceAll(ctRegex!(`["“”\.]`), "")
.toLower;
}
struct PlantData {
Species[] species;
Plant[] plants;
Plant[] plantsInSpecies(Species speciesItem) {
Appender!(Plant[]) app;
foreach (plant; plants) {
if (plant.speciesScientificName == speciesItem.scientificName) {
app ~= plant;
}
}
Plant[] results = app[];
sort!((a, b) => a.identifier < b.identifier)(results);
return results;
}
Species getSpecies(string name) {
foreach (s; species) {
if (s.scientificName == name) return s;
}
throw new Exception("No species with name " ~ name);
}
Plant getPlant(string identifier) {
foreach (p; plants) {
if (p.identifier == identifier) return p;
}
throw new Exception("No plant with identifier " ~ identifier);
}
}
struct ImageFilePair {
string filename;
uint width, height;
string thumbnailFilename;
uint thumbnailWidth, thumbnailHeight;
}
PlantData parsePlantData(string filename) {
import archive.zip;
ZipArchive zip = new ZipArchive(std.file.read(filename));
auto contentZipEntry = zip.getFile("content.xml");
if (contentZipEntry is null) throw new Exception("Couldn't find content.xml in " ~ filename);
DOMEntity!string dom = parseDOM(cast(string) contentZipEntry.data());
DOMEntity!string spreadsheet = dom.findDOMChild("office:document-content")
.findDOMChild("office:body")
.findDOMChild("office:spreadsheet");
DOMEntity!string speciesTable = spreadsheet.findDOMChild("table:table", ["table:name": "Species"]);
DOMEntity!string[] speciesRows = speciesTable.findDOMChildren("table:table-row")[1..$];
DOMEntity!string plantsTable = spreadsheet.findDOMChild("table:table", ["table:name": "Plants"]);
DOMEntity!string[] plantRows = plantsTable.findDOMChildren("table:table-row")[1..$];
PlantData result;
auto speciesAppender = appender(&result.species);
foreach (row; speciesRows) {
if (row.children.length < 4) continue;
Species species;
species.name = readTableCellText(row.children[0]);
species.scientificName = readTableCellText(row.children[1]);
species.description = readTableCellText(row.children[2]);
species.referenceLink = readTableCellText(row.children[3]);
species.id = speciesId(species.scientificName);
speciesAppender ~= species;
}
sort!((a, b) => a.name < b.name)(result.species);
auto plantAppender = appender(&result.plants);
foreach (row; plantRows) {
import std.string : strip;
if (row.children.length < 4) continue;
Plant plant;
plant.speciesScientificName = readTableCellText(row.children[0]);
if (plant.speciesScientificName.strip.length == 0) continue;
plant.identifier = readTableCellText(row.children[1]);
string fGenStr = readTableCellText(row.children[2]);
import std.conv : to;
plant.generation = fGenStr[1..$].to!uint;
plant.plantingInfo = readTableCellText(row.children[3]);
if (row.children.length > 4) {
plant.description = readTableCellText(row.children[4]);
}
plantAppender ~= plant;
}
sort!((a, b) => a.identifier < b.identifier)(result.plants);
return result;
}
void ensureDirectories(PlantData data) {
string basePath = buildPath("images", "garden");
if (!exists(basePath)) mkdirRecurse(basePath);
string speciesDir = buildPath(basePath, "species");
if (!exists(speciesDir)) mkdir(speciesDir);
foreach (s; data.species) {
string thisSpeciesDir = buildPath(speciesDir, s.id);
if (!exists(thisSpeciesDir)) mkdir(thisSpeciesDir);
}
string plantsDir = buildPath(basePath, "plants");
if (!exists(plantsDir)) mkdir(plantsDir);
foreach (p; data.plants) {
string thisPlantDir = buildPath(plantsDir, p.identifier);
if (!exists(thisPlantDir)) mkdir(thisPlantDir);
}
}
ImageFilePair[] getPlantImages(string identifier) {
string plantDir = buildPath("images", "garden", "plants", identifier);
if (!exists(plantDir)) return [];
Appender!(ImageFilePair[]) app;
foreach (entry; dirEntries(plantDir, SpanMode.shallow, false)) {
if (entry.name.endsWith(".jpg") && !entry.name.endsWith(".thumb.jpg")) {
ImageFilePair pair;
pair.filename = entry.name;
getImageSize(entry.name, pair.width, pair.height);
string thumbnailFilename = buildPath(plantDir, baseName(entry.name, ".jpg") ~ ".thumb.jpg");
if (exists(thumbnailFilename)) {
pair.thumbnailFilename = thumbnailFilename;
getImageSize(thumbnailFilename, pair.thumbnailWidth, pair.thumbnailHeight);
}
app ~= pair;
}
}
ImageFilePair[] images = app[];
sort!((a, b) {
Nullable!DateTime tsA = getImageTimestamp(a.filename);
Nullable!DateTime tsB = getImageTimestamp(b.filename);
if (tsA.isNull && tsB.isNull) return a.filename < b.filename;
if (tsA.isNull) return true;
if (tsB.isNull) return false;
return tsA.get < tsB.get;
})(images);
return images;
}
ImageFilePair[] getSpeciesImages(string speciesId) {
string speciesDir = buildPath("images", "garden", "species", speciesId);
if (!exists(speciesDir)) return [];
Appender!(ImageFilePair[]) app;
foreach (entry; dirEntries(speciesDir, SpanMode.shallow, false)) {
if (entry.name.endsWith(".jpg") && !entry.name.endsWith(".thumb.jpg")) {
ImageFilePair pair;
pair.filename = entry.name;
getImageSize(entry.name, pair.width, pair.height);
string thumbnailFilename = buildPath(speciesDir, baseName(entry.name, ".jpg") ~ ".thumb.jpg");
if (exists(thumbnailFilename)) {
pair.thumbnailFilename = thumbnailFilename;
getImageSize(thumbnailFilename, pair.thumbnailWidth, pair.thumbnailHeight);
}
app ~= pair;
}
}
ImageFilePair[] images = app[];
sort!((a, b) {
Nullable!DateTime tsA = getImageTimestamp(a.filename);
Nullable!DateTime tsB = getImageTimestamp(b.filename);
if (tsA.isNull && tsB.isNull) return a.filename < b.filename;
if (tsA.isNull) return true;
if (tsB.isNull) return false;
return tsA.get < tsB.get;
})(images);
return images;
}
void getImageSize(string filePath, out uint width, out uint height) {
import std.process;
import std.format;
auto result = execute(["identify", "-ping", "-format", "'%w %h'", filePath]);
if (result.status != 0) throw new Exception("Failed to get image size of " ~ filePath);
formattedRead!"'%d %d'"(result.output, width, height);
}
Nullable!DateTime getImageTimestamp(string filePath) {
import std.regex;
import std.conv;
auto r = ctRegex!(`\d{8}_\d{6}`);
auto cap = matchFirst(baseName(filePath), r);
if (cap.empty) return Nullable!DateTime.init;
string text = cap[0];
return nullable(DateTime(
text[0..4].to!int,
text[4..6].to!int,
text[6..8].to!int,
text[9..11].to!int,
text[11..13].to!int,
text[13..15].to!int
));
}
void generateAllThumbnails(bool regen = false) {
string plantsDir = buildPath("images", "garden", "plants");
foreach (entry; dirEntries(plantsDir, SpanMode.shallow, false)) {
generateThumbnails(entry.name, regen);
}
string speciesDir = buildPath("images", "garden", "species");
foreach (entry; dirEntries(speciesDir, SpanMode.shallow, false)) {
generateThumbnails(entry.name, regen);
}
}
void generateThumbnails(string dir, bool regen) {
import std.process;
if (regen) {
// Remove all thumbnails first.
foreach (entry; dirEntries(dir, SpanMode.shallow, false)) {
if (entry.name.endsWith(".thumb.jpg")) {
std.file.remove(entry.name);
}
}
}
foreach (entry; dirEntries(dir, SpanMode.shallow, false)) {
if (entry.name.endsWith(".jpg") && !entry.name.endsWith(".thumb.jpg")) {
string filenameWithoutExt = baseName(entry.name, ".jpg");
string outputFilePath = buildPath(dir, filenameWithoutExt ~ ".thumb.jpg");
if (exists(outputFilePath)) continue;
Pid pid = spawnProcess(
[
"convert",
entry.name,
"-strip",
"-interlace", "JPEG",
"-sampling-factor", "4:2:0",
"-colorspace", "RGB",
"-quality", "85%",
"-geometry", "x200",
outputFilePath
]
);
int exitCode = wait(pid);
if (exitCode != 0) throw new Exception("Thumbnail generation process failed.");
}
}
}