273 lines
9.5 KiB
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.");
|
||
}
|
||
}
|
||
}
|