mirror of
https://github.com/sbrl/terrain50-cli.git
synced 2024-11-22 06:53:01 +00:00
image: Refactor everything that isn't cli handling into manager class
The cli handling belongs in image/index.mjs, but everything that isn't cli handling belongs in a middleware class kinda thing. In this case, we've created a RenderManager class.
This commit is contained in:
parent
d94bed2f1b
commit
6ec176d895
6 changed files with 152 additions and 40 deletions
28
README.md
28
README.md
|
@ -31,6 +31,34 @@ If you installed it locally, you'll need to do this:
|
||||||
path/to/node_modules/.bin/terrain50 --help
|
path/to/node_modules/.bin/terrain50 --help
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
Additionally, a number of environment variables are supported.
|
||||||
|
|
||||||
|
Variable | Purpose
|
||||||
|
------------|-----------------------------
|
||||||
|
`NO_COLOR` | Disables ANSI escape codes in output (i.e. coloured output). Not recommended unless you have a reason.
|
||||||
|
`QUIET` | Suppress all output except for warnings and errors (not fully supported everywhere yet)
|
||||||
|
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
### `image` subcommand: `--boundaries` argument
|
||||||
|
This argument's purpose is the divide the incoming data into categories so that an AI can be potentially trained on the data (e.g. water depth data, as I'm using). It takes a comma separated list of values like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
0.1,0.5,1,5
|
||||||
|
```
|
||||||
|
|
||||||
|
...and turns it into a number of bins like so:
|
||||||
|
|
||||||
|
- -Infinity ≤ value < 0.1
|
||||||
|
- 0.1 ≤ value < 0.5
|
||||||
|
- 0.5 ≤ value < 1
|
||||||
|
- 1 ≤ value < 5
|
||||||
|
- 5 ≤ value < Infinity
|
||||||
|
|
||||||
|
Each bin is assigned a colour. Then, for each value in the input, it draws the colour that's assigned to the bin that the value fits into.
|
||||||
|
|
||||||
|
|
||||||
## Read-world use
|
## Read-world use
|
||||||
- I'm using it for the main Node.js application for my PhD in Computer Science!
|
- I'm using it for the main Node.js application for my PhD in Computer Science!
|
||||||
|
|
|
@ -12,4 +12,23 @@ function percentage(count_so_far, total, range=100) {
|
||||||
return (count_so_far/total)*range;
|
return (count_so_far/total)*range;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { percentage };
|
/**
|
||||||
|
* Converts the given array of boundaries to an array of class objects.
|
||||||
|
* @source PhD-Code - common/ai/ModelHelper.mjs
|
||||||
|
* @param {Number[]} bounds The bounds to convert.
|
||||||
|
* @return {{min:Number,max:Number}[]} An array of objects that representa single class each, with min / max values for each class.
|
||||||
|
*/
|
||||||
|
function bounds2classes(bounds) {
|
||||||
|
if(bounds.length < 2)
|
||||||
|
throw new Exception(`Error: Not enough bounds supplied (got ${bounds.length}, but expected at least 2)`);
|
||||||
|
|
||||||
|
let result = [];
|
||||||
|
result.push({ min: -Infinity, max: bounds[0] });
|
||||||
|
for(let i = 1; i < bounds.length; i++) {
|
||||||
|
result.push({ min: bounds[i - 1], max: bounds[i] });
|
||||||
|
}
|
||||||
|
result.push({ min: bounds[bounds.length - 1], max: Infinity });
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { percentage, bounds2classes };
|
||||||
|
|
67
src/Subcommands/image/RenderManager.mjs
Normal file
67
src/Subcommands/image/RenderManager.mjs
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
import Terrain50 from 'terrain50';
|
||||||
|
|
||||||
|
import l from '../../Helpers/Log.mjs';
|
||||||
|
import a from '../../Helpers/Ansi.mjs';
|
||||||
|
import Terrain50Renderer from './Terrain50Renderer.mjs';
|
||||||
|
|
||||||
|
|
||||||
|
class RenderManager {
|
||||||
|
constructor(scale_factor = 1, in_tolerant = false, in_classes = null) {
|
||||||
|
this.quiet = typeof process.env.QUIET == "string" ? true : false;
|
||||||
|
|
||||||
|
this.tolerant = in_tolerant;
|
||||||
|
this.classes = in_classes;
|
||||||
|
|
||||||
|
this.renderer = new Terrain50Renderer(scale_factor);
|
||||||
|
}
|
||||||
|
|
||||||
|
async render_one_filename(filename_in, filename_out) {
|
||||||
|
let terrain50 = Terrain50.Parse(
|
||||||
|
await fs.promises.readFile(filename_in, "utf-8")
|
||||||
|
);
|
||||||
|
let png_buffer = await this.renderer.render(terrain50, this.classes);
|
||||||
|
if(!(png_buffer instanceof Buffer))
|
||||||
|
throw new Error(`Error: Renderer did not return Buffer (found unexpected ${png_buffer} instead)`);
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
filename_out,
|
||||||
|
png_buffer
|
||||||
|
);
|
||||||
|
|
||||||
|
if(!this.quiet) l.log(`Written to ${a.hicol}${filename_out}${a.reset}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async render_many_filename(filename_in, dir_out) {
|
||||||
|
let reader = process.stdin;
|
||||||
|
if(filename_in !== "-")
|
||||||
|
reader = fs.createReadStream(filename_in, "utf-8");
|
||||||
|
|
||||||
|
await this.render_many_stream(reader, dir_out);
|
||||||
|
}
|
||||||
|
|
||||||
|
async render_many_stream(stream_in, dir_out) {
|
||||||
|
if(!fs.existsSync(dir_out))
|
||||||
|
await fs.promises.mkdir(dir_out, { recursive: true, mode: 0o755 });
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
for await(let next of Terrain50.ParseStream(stream_in, this.tolerant ? /\s+/ : " ")) {
|
||||||
|
if(!this.quiet) process.stderr.write(`${a.fgreen}>>>>> ${a.hicol} Item ${i} ${a.reset}${a.fgreen} <<<<<${a.reset}\n`);
|
||||||
|
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(dir_out, `${i}.png`),
|
||||||
|
await renderer.render(next, this.classes)
|
||||||
|
);
|
||||||
|
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!this.quiet) l.log(`Written ${a.hicol}${i}${a.reset} items to ${a.hicol}${a.fgreen}${dir_out}${a.reset}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RenderManager;
|
|
@ -24,9 +24,10 @@ class Terrain50Renderer {
|
||||||
* You probably want the .render() method, which returns a buffer
|
* You probably want the .render() method, which returns a buffer
|
||||||
* containing a png-encoded image.
|
* containing a png-encoded image.
|
||||||
* @param {Terrain50} terrain The Terrain50 object instance to render.
|
* @param {Terrain50} terrain The Terrain50 object instance to render.
|
||||||
|
* @param {[number, number][]} classes The classes to bin the values into. If not specified, values are not binned into classes. Warning: Values *must* fit into a bin. It is recommended to use -Infinity and Infinity in the first and last bins.
|
||||||
* @return {ArrayBuffer} A canvas with the image rendered on it.
|
* @return {ArrayBuffer} A canvas with the image rendered on it.
|
||||||
*/
|
*/
|
||||||
async do_render(terrain) {
|
async do_render(terrain, classes = null) {
|
||||||
let colour_domain = null;
|
let colour_domain = null;
|
||||||
if(this.colour_domain === "auto") {
|
if(this.colour_domain === "auto") {
|
||||||
let min = terrain.min_value, max = terrain.max_value;
|
let min = terrain.min_value, max = terrain.max_value;
|
||||||
|
@ -103,14 +104,15 @@ class Terrain50Renderer {
|
||||||
* Renders the given Terrain50 object to an image.
|
* Renders the given Terrain50 object to an image.
|
||||||
* Returns a buffer containing a PNG-encoded image, which is ready to be
|
* Returns a buffer containing a PNG-encoded image, which is ready to be
|
||||||
* written to disk for example.
|
* written to disk for example.
|
||||||
* @param {Terrain50} terrain The terrain object to render.
|
* @param {Terrain50} terrain The terrain object to render.
|
||||||
|
* @param {[number, number][]} classes The classes to bin the values into. If not specified, values are not binned into classes. Warning: Values *must* fit into a bin. It is recommended to use -Infinity and Infinity in the first and last bins.
|
||||||
* @return {Buffer} The terrain object as a png, represented as a buffer.
|
* @return {Buffer} The terrain object as a png, represented as a buffer.
|
||||||
*/
|
*/
|
||||||
async render(terrain) {
|
async render(terrain, classes = null) {
|
||||||
let width = Math.floor(terrain.meta.ncols / this.scale_factor),
|
let width = Math.floor(terrain.meta.ncols / this.scale_factor),
|
||||||
height = Math.floor(terrain.meta.nrows / this.scale_factor);
|
height = Math.floor(terrain.meta.nrows / this.scale_factor);
|
||||||
|
|
||||||
let result = await this.do_render(terrain);
|
let result = await this.do_render(terrain, classes);
|
||||||
return Buffer.from(encode(result, [ width, height ], "png"));
|
return Buffer.from(encode(result, [ width, height ], "png"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,10 +6,9 @@ import path from 'path';
|
||||||
import l from '../../Helpers/Log.mjs';
|
import l from '../../Helpers/Log.mjs';
|
||||||
import a from '../../Helpers/Ansi.mjs';
|
import a from '../../Helpers/Ansi.mjs';
|
||||||
|
|
||||||
import Terrain50 from 'terrain50';
|
import RenderManager from './RenderManager.mjs';
|
||||||
import Terrain50Renderer from './Terrain50Renderer.mjs';
|
|
||||||
|
|
||||||
import { write_safe, end_safe } from '../../Helpers/StreamHelpers.mjs';
|
import bounds2classes from '../../Helpers/MathsHelpers.mjs';
|
||||||
|
|
||||||
export default async function(settings) {
|
export default async function(settings) {
|
||||||
if(typeof settings.cli.input !== "string") {
|
if(typeof settings.cli.input !== "string") {
|
||||||
|
@ -26,42 +25,34 @@ export default async function(settings) {
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
let renderer = new Terrain50Renderer(settings.scale_factor | 1);
|
// Parse the bounaries out
|
||||||
|
if(typeof settings.cli.boundaries == "string") {
|
||||||
|
settings.cli.boundaries = settings.cli.boundaries
|
||||||
|
.split(",")
|
||||||
|
.map((value) => parseFloat(value.trim()))
|
||||||
|
.filter((x) => typeof x == "number");
|
||||||
|
|
||||||
|
settings.classes = bounds2classes(settings.cli.boundaries);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
settings.classes = null;
|
||||||
|
|
||||||
|
let render_manager = new RenderManager(
|
||||||
|
settings.scale_factor || 1,
|
||||||
|
settings.cli.tolerant,
|
||||||
|
settings.classes
|
||||||
|
);
|
||||||
|
|
||||||
if(settings.cli.stream) {
|
if(settings.cli.stream) {
|
||||||
if(!fs.existsSync(settings.cli.output))
|
await render_manager.render_many_filename(
|
||||||
await fs.promises.mkdir(settings.cli.output, { recursive: true, mode: 0o755 });
|
settings.cli.input,
|
||||||
|
settings.cli.output
|
||||||
let reader = process.stdin;
|
);
|
||||||
if(settings.cli.input !== "-")
|
|
||||||
reader = fs.createReadStream(settings.cli.input, "utf-8");
|
|
||||||
|
|
||||||
let i = 0;
|
|
||||||
for await(let next of Terrain50.ParseStream(reader, settings.cli.tolerant ? /\s+/ : " ")) {
|
|
||||||
process.stderr.write(`${a.fgreen}>>>>> ${a.hicol} Item ${i} ${a.reset}${a.fgreen} <<<<<${a.reset}\n`);
|
|
||||||
|
|
||||||
await fs.promises.writeFile(
|
|
||||||
path.join(settings.cli.output, `${i}.png`),
|
|
||||||
await renderer.render(next)
|
|
||||||
);
|
|
||||||
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
l.log(`Written ${a.hicol}${i}${a.reset} items to ${a.hicol}${a.fgreen}${settings.cli.output}${a.reset}`);
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
let terrain50 = Terrain50.Parse(
|
await render_manager.render_one_filename(
|
||||||
await fs.promises.readFile(settings.cli.input, "utf-8")
|
settings.cli.input,
|
||||||
|
settings.cli.output
|
||||||
);
|
);
|
||||||
let png_buffer = await renderer.render(terrain50);
|
|
||||||
if(!(png_buffer instanceof Buffer))
|
|
||||||
throw new Error(`Error: Renderer did not return Buffer (found unexpected ${png_buffer} instead)`);
|
|
||||||
await fs.promises.writeFile(
|
|
||||||
settings.cli.output,
|
|
||||||
png_buffer
|
|
||||||
);
|
|
||||||
|
|
||||||
l.log(`Written to ${a.hicol}${settings.cli.output}${a.reset}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,3 +17,8 @@ name = "stream"
|
||||||
description = "Treat the input as a stream of Terrain50 objects. The path specified by the --output argument will be a directory containing all the extracted objects, 1 image file per object."
|
description = "Treat the input as a stream of Terrain50 objects. The path specified by the --output argument will be a directory containing all the extracted objects, 1 image file per object."
|
||||||
default_value = false
|
default_value = false
|
||||||
type = "boolean"
|
type = "boolean"
|
||||||
|
|
||||||
|
[[arguments]]
|
||||||
|
name = "boundaries"
|
||||||
|
description = "Takes a comma-separated list of numbers. If specified, values will be binned into categories that match the given boundaries. See the note in the README for more information."
|
||||||
|
type = "string"
|
||||||
|
|
Loading…
Reference in a new issue