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

273 lines
9.5 KiB
D
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.");
}
}
}