Skip to content

Commit 30904ff

Browse files
committed
Update the cogify create cog to support topo raster
1 parent b8bedc3 commit 30904ff

File tree

5 files changed

+111
-44
lines changed

5 files changed

+111
-44
lines changed

packages/cogify/src/cogify/cli/cli.cog.ts

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { isEmptyTiff } from '@basemaps/config-loader';
2-
import { Projection, ProjectionLoader, TileId, TileMatrixSet, TileMatrixSets } from '@basemaps/geo';
2+
import { Projection, ProjectionLoader, TileMatrixSet, TileMatrixSets } from '@basemaps/geo';
33
import { fsa, LogType, stringToUrlFolder, Tiff } from '@basemaps/shared';
44
import { CliId, CliInfo } from '@basemaps/shared/build/cli/info.js';
55
import { Metrics } from '@linzjs/metrics';
@@ -155,7 +155,7 @@ export const BasemapsCogifyCreateCommand = command({
155155
const { item, url } = f;
156156
const cutlineLink = getCutline(item.links);
157157
const options = item.properties['linz_basemaps:options'];
158-
const tileId = TileId.fromTile(options.tile);
158+
const tileId = options.tileId;
159159

160160
// Location to where the tiff should be stored
161161
const tiffPath = new URL(tileId + '.tiff', url);
@@ -268,7 +268,7 @@ export const BasemapsCogifyCreateCommand = command({
268268
{
269269
count: toCreate.length,
270270
created: filtered.length,
271-
files: filtered.map((f) => TileId.fromTile(f.item.properties['linz_basemaps:options'].tile)),
271+
files: filtered.map((f) => f.item.properties['linz_basemaps:options'].tileId),
272272
},
273273
'Cog:Done',
274274
);
@@ -292,7 +292,7 @@ export interface CogCreationContext {
292292
async function createCog(ctx: CogCreationContext): Promise<URL> {
293293
const options = ctx.options;
294294
await ProjectionLoader.load(options.sourceEpsg);
295-
const tileId = TileId.fromTile(options.tile);
295+
const tileId = options.tileId;
296296

297297
const logger = ctx.logger?.child({ tileId });
298298

@@ -303,13 +303,13 @@ async function createCog(ctx: CogCreationContext): Promise<URL> {
303303

304304
logger?.debug({ tileId }, 'Cog:Create:VrtSource');
305305
// Create the vrt of all the source files
306-
const vrtSourceCommand = gdalBuildVrt(new URL(`${tileId}-source.vrt`, ctx.tempFolder), ctx.sourceFiles);
306+
const vrtSourceCommand = gdalBuildVrt(new URL(`${tileId}-source.vrt`, ctx.tempFolder), ctx.sourceFiles, options);
307307
await new GdalRunner(vrtSourceCommand).run(logger);
308308

309309
logger?.debug({ tileId }, 'Cog:Create:VrtWarp');
310310

311311
const cutlineProperties: { url: URL | null; blend: number } = { url: null, blend: ctx.cutline.blend };
312-
if (ctx.cutline.path) {
312+
if (ctx.cutline.path && options.tile) {
313313
logger?.debug('Cog:Cutline');
314314
const optimizedCutline = ctx.cutline.optimize(options.tile);
315315
if (optimizedCutline) {
@@ -321,19 +321,23 @@ async function createCog(ctx: CogCreationContext): Promise<URL> {
321321
}
322322
}
323323

324-
// warp the source VRT into the output parameters
325-
const vrtWarpCommand = gdalBuildVrtWarp(
326-
new URL(`${tileId}-${options.tileMatrix}-warp.vrt`, ctx.tempFolder),
327-
vrtSourceCommand.output,
328-
options.sourceEpsg,
329-
cutlineProperties,
330-
options,
331-
);
332-
await new GdalRunner(vrtWarpCommand).run(logger);
324+
let vrtOutput = vrtSourceCommand.output;
325+
if (!options.noReprojecting) {
326+
// warp the source VRT into the output parameters
327+
const vrtWarpCommand = gdalBuildVrtWarp(
328+
new URL(`${tileId}-${options.tileMatrix}-warp.vrt`, ctx.tempFolder),
329+
vrtSourceCommand.output,
330+
options.sourceEpsg,
331+
cutlineProperties,
332+
options,
333+
);
334+
await new GdalRunner(vrtWarpCommand).run(logger);
335+
vrtOutput = vrtWarpCommand.output;
336+
}
333337

334338
if (options.background == null) {
335339
// Create the COG from the warped vrt without a forced background
336-
const cogCreateCommand = gdalBuildCog(new URL(`${tileId}.tiff`, ctx.tempFolder), vrtWarpCommand.output, options);
340+
const cogCreateCommand = gdalBuildCog(new URL(`${tileId}.tiff`, ctx.tempFolder), vrtOutput, options);
337341
await new GdalRunner(cogCreateCommand).run(logger);
338342
return cogCreateCommand.output;
339343
}
@@ -345,7 +349,7 @@ async function createCog(ctx: CogCreationContext): Promise<URL> {
345349
// Create a vrt with the background tiff behind the source file vrt
346350
const vrtMergeCommand = gdalBuildVrt(new URL(`${tileId}-merged.vrt`, ctx.tempFolder), [
347351
gdalCreateCommand.output,
348-
vrtWarpCommand.output,
352+
vrtOutput,
349353
]);
350354
await new GdalRunner(vrtMergeCommand).run(logger);
351355

packages/cogify/src/cogify/cli/cli.cover.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,11 +144,13 @@ export const BasemapsCogifyCoverCommand = command({
144144
const items = [];
145145
const tilesByZoom: number[] = [];
146146
for (const item of res.items) {
147-
const tileId = TileId.fromTile(item.properties['linz_basemaps:options'].tile);
147+
const tile = item.properties['linz_basemaps:options'].tile;
148+
if (tile == null) throw new Error('Tile not found in item');
149+
const tileId = TileId.fromTile(tile);
148150
const itemPath = new URL(`${tileId}.json`, targetPath);
149151
items.push({ path: itemPath });
150152
await fsa.write(itemPath, JSON.stringify(item, null, 2));
151-
const z = item.properties['linz_basemaps:options'].tile.z;
153+
const z = tile.z;
152154
tilesByZoom[z] = (tilesByZoom[z] ?? 0) + 1;
153155
ctx.logger?.trace({ path: itemPath }, 'Imagery:Stac:Item:Write');
154156
}

packages/cogify/src/cogify/gdal.command.ts

Lines changed: 50 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,46 @@ import { CogifyCreationOptions } from './stac.js';
88

99
const isPowerOfTwo = (x: number): boolean => (x & (x - 1)) === 0;
1010

11-
export function gdalBuildVrt(targetVrt: URL, source: URL[]): GdalCommand {
11+
interface TargetOptions {
12+
targetSrs?: string;
13+
extent?: number[];
14+
targetResolution?: number;
15+
}
16+
17+
function getTargetOptions(opt: CogifyCreationOptions): TargetOptions {
18+
const targetOpts: TargetOptions = {};
19+
20+
if (opt.tileMatrix) {
21+
const tileMatrix = TileMatrixSets.find(opt.tileMatrix);
22+
if (tileMatrix == null) throw new Error('Unable to find tileMatrix: ' + opt.tileMatrix);
23+
targetOpts.targetSrs = tileMatrix.projection.toEpsgString();
24+
25+
if (opt.tile) {
26+
const bounds = tileMatrix.tileToSourceBounds(opt.tile);
27+
targetOpts.extent = [
28+
Math.min(bounds.x, bounds.right),
29+
Math.min(bounds.y, bounds.bottom),
30+
Math.max(bounds.x, bounds.right),
31+
Math.max(bounds.y, bounds.bottom),
32+
];
33+
}
34+
35+
if (opt.zoomLevel) {
36+
targetOpts.targetResolution = tileMatrix.pixelScale(opt.zoomLevel);
37+
}
38+
}
39+
return targetOpts;
40+
}
41+
42+
export function gdalBuildVrt(targetVrt: URL, source: URL[], opt?: CogifyCreationOptions): GdalCommand {
1243
if (source.length === 0) throw new Error('No source files given for :' + targetVrt.href);
1344
return {
1445
output: targetVrt,
1546
command: 'gdalbuildvrt',
16-
args: [urlToString(targetVrt), ...source.map(urlToString)],
47+
args: [opt && opt.addalpha ? ['-addalpha'] : undefined, urlToString(targetVrt), ...source.map(urlToString)]
48+
.filter((f) => f != null)
49+
.flat()
50+
.map(String),
1751
};
1852
}
1953

@@ -26,6 +60,7 @@ export function gdalBuildVrtWarp(
2660
): GdalCommand {
2761
const tileMatrix = TileMatrixSets.find(opt.tileMatrix);
2862
if (tileMatrix == null) throw new Error('Unable to find tileMatrix: ' + opt.tileMatrix);
63+
if (opt.zoomLevel == null) throw new Error('Unable to find zoomLevel');
2964
const targetResolution = tileMatrix.pixelScale(opt.zoomLevel);
3065

3166
return {
@@ -53,18 +88,8 @@ export function gdalBuildVrtWarp(
5388

5489
export function gdalBuildCog(targetTiff: URL, sourceVrt: URL, opt: CogifyCreationOptions): GdalCommand {
5590
const cfg = { ...Presets[opt.preset], ...opt };
56-
const tileMatrix = TileMatrixSets.find(cfg.tileMatrix);
57-
if (tileMatrix == null) throw new Error('Unable to find tileMatrix: ' + cfg.tileMatrix);
5891

59-
const bounds = tileMatrix.tileToSourceBounds(cfg.tile);
60-
const tileExtent = [
61-
Math.min(bounds.x, bounds.right),
62-
Math.min(bounds.y, bounds.bottom),
63-
Math.max(bounds.x, bounds.right),
64-
Math.max(bounds.y, bounds.bottom),
65-
];
66-
67-
const targetResolution = tileMatrix.pixelScale(cfg.zoomLevel);
92+
const targetOpts = getTargetOptions(cfg);
6893

6994
return {
7095
command: 'gdal_translate',
@@ -73,25 +98,27 @@ export function gdalBuildCog(targetTiff: URL, sourceVrt: URL, opt: CogifyCreatio
7398
['-of', 'COG'],
7499
['-co', 'NUM_THREADS=ALL_CPUS'], // Use all CPUS
75100
['--config', 'GDAL_NUM_THREADS', 'all_cpus'], // Also required to NUM_THREADS till gdal 3.7.x
76-
['-co', 'BIGTIFF=IF_NEEDED'], // BigTiff is somewhat slower and most (All?) of the COGS should be well below 4GB
101+
cfg.srcwin ? ['-srcwin', cfg.srcwin[0], cfg.srcwin[1], cfg.srcwin[2], cfg.srcwin[3]] : undefined,
102+
cfg.bigTIFF ? ['-co', `BIGTIFF=${cfg.bigTIFF}`] : ['-co', 'BIGTIFF=IF_NEEDED'], // BigTiff is somewhat slower and most (All?) of the COGS should be well below 4GB
77103
['-co', 'ADD_ALPHA=YES'],
104+
['-co', `BLOCKSIZE=${cfg.blockSize}`],
78105
/**
79106
* GDAL will recompress existing overviews if they exist which will compound
80107
* any lossly compression on the overview, so compute new overviews instead
81108
*/
82109
['-co', 'OVERVIEWS=IGNORE_EXISTING'],
83-
['-co', `BLOCKSIZE=${cfg.blockSize}`],
84-
// ['-co', 'RESAMPLING=cubic'],
85-
['-co', `WARP_RESAMPLING=${cfg.warpResampling}`],
86-
['-co', `OVERVIEW_RESAMPLING=${cfg.overviewResampling}`],
110+
cfg.overviewCompress ? ['-co', `OVERVIEW_COMPRESS=${cfg.overviewCompress}`] : undefined,
111+
cfg.overviewQuality ? ['-co', `OVERVIEW_QUALITY=${cfg.overviewQuality}`] : undefined,
112+
cfg.warpResampling ? ['-co', `WARP_RESAMPLING=${cfg.warpResampling}`] : undefined,
113+
cfg.overviewResampling ? ['-co', `OVERVIEW_RESAMPLING=${cfg.overviewResampling}`] : undefined,
87114
['-co', `COMPRESS=${cfg.compression}`],
88115
cfg.quality ? ['-co', `QUALITY=${cfg.quality}`] : undefined,
89116
cfg.maxZError ? ['-co', `MAX_Z_ERROR=${cfg.maxZError}`] : undefined,
90117
cfg.maxZErrorOverview ? ['-co', `MAX_Z_ERROR_OVERVIEW=${cfg.maxZErrorOverview}`] : undefined,
91118
['-co', 'SPARSE_OK=YES'],
92-
['-co', `TARGET_SRS=${tileMatrix.projection.toEpsgString()}`],
93-
['-co', `EXTENT=${tileExtent.join(',')},`],
94-
['-tr', targetResolution, targetResolution],
119+
targetOpts.targetSrs ? ['-co', `TARGET_SRS=${targetOpts.targetSrs}`] : undefined,
120+
targetOpts.extent ? ['-co', `EXTENT=${targetOpts.extent.join(',')},`] : undefined,
121+
targetOpts.targetResolution ? ['-tr', targetOpts.targetResolution, targetOpts.targetResolution] : undefined,
95122
urlToString(sourceVrt),
96123
urlToString(targetTiff),
97124
]
@@ -117,8 +144,8 @@ export function gdalCreate(targetTiff: URL, color: Rgba, opt: CogifyCreationOpti
117144
const tileMatrix = TileMatrixSets.find(cfg.tileMatrix);
118145
if (tileMatrix == null) throw new Error('Unable to find tileMatrix: ' + cfg.tileMatrix);
119146

120-
const bounds = tileMatrix.tileToSourceBounds(cfg.tile);
121-
const pixelScale = tileMatrix.pixelScale(cfg.zoomLevel);
147+
const bounds = tileMatrix.tileToSourceBounds(cfg.tile ?? { x: 0, y: 0, z: 0 });
148+
const pixelScale = tileMatrix.pixelScale(cfg.zoomLevel ?? 0);
122149
const size = Math.round(bounds.width / pixelScale);
123150

124151
// if the value of 'size' is not a power of 2

packages/cogify/src/cogify/stac.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@ export interface CogifyCreationOptions {
88
/** Preset GDAL config to use */
99
preset: string;
1010

11+
/** Tile Id to be created */
12+
tileId: string;
13+
1114
/** Tile to be created */
12-
tile: Tile;
15+
tile?: Tile;
1316

1417
/** Tile matrix to create the tiles against */
1518
tileMatrix: string;
@@ -58,8 +61,36 @@ export interface CogifyCreationOptions {
5861
*/
5962
overviewResampling?: GdalResampling;
6063

64+
/**
65+
* compression method for overview
66+
*/
67+
overviewCompress?: string;
68+
69+
/**
70+
* JPEG/WEBP quality setting for overviews range from 1 to 100
71+
*/
72+
overviewQuality?: number;
73+
6174
/** Color with which to replace all transparent COG pixels */
6275
background?: Rgba;
76+
77+
/** Adds an alpha mask band to the VRT when the source raster have none. */
78+
addalpha?: boolean;
79+
80+
/** Stop to reproject the imagery by gdalwarp*/
81+
noReprojecting?: boolean;
82+
83+
/**
84+
* External overviews can be created in the BigTIFF format
85+
*
86+
* @default IF_NEEDED
87+
*/
88+
bigTIFF?: 'YES' | 'NO' | 'IF_NEEDED' | 'IF_SAFER';
89+
90+
/**
91+
* Selects a subwindow from the source image for copying based on pixel/line location.
92+
*/
93+
srcwin?: number[];
6394
}
6495

6596
export type GdalResampling = 'nearest' | 'bilinear' | 'cubic' | 'cubicspline' | 'lanczos' | 'average' | 'mode';

packages/cogify/src/tile.cover.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ export async function createTileCover(ctx: TileCoverContext): Promise<TileCoverR
168168
'linz_basemaps:options': {
169169
preset: ctx.preset,
170170
...Presets[ctx.preset].options,
171+
tileId: TileId.fromTile(tile),
171172
tile,
172173
tileMatrix: ctx.tileMatrix.identifier,
173174
sourceEpsg: ctx.imagery.projection,
@@ -225,7 +226,9 @@ export async function createTileCover(ctx: TileCoverContext): Promise<TileCoverR
225226
temporal: { interval: dateTime.start ? [[dateTime.start, dateTime.end]] : [[cliDate, null]] },
226227
},
227228
links: items.map((item) => {
228-
const tileId = TileId.fromTile(item.properties['linz_basemaps:options'].tile);
229+
const tile = item.properties['linz_basemaps:options'].tile;
230+
if (tile == null) throw new Error('Tile missing from item');
231+
const tileId = TileId.fromTile(tile);
229232
return { href: `./${tileId}.json`, rel: 'item', type: 'application/json' };
230233
}),
231234
};

0 commit comments

Comments
 (0)