viewtif 0.2.5__py3-none-any.whl → 0.2.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
viewtif/tif_viewer.py CHANGED
@@ -15,7 +15,7 @@ Controls
15
15
  Arrow keys or WASD : pan
16
16
  C / V : increase/decrease contrast (works in RGB and single-band)
17
17
  G / H : increase/decrease gamma (works in RGB and single-band)
18
- M : toggle colormap (viridis <-> magma) — single-band only
18
+ M : toggle colormap. Single-band: viridis/magma. NetCDF: RdBu_r/viridis/magma.
19
19
  [ / ] : previous / next band (or time step) (single-band)
20
20
  R : reset view
21
21
 
@@ -27,13 +27,12 @@ Examples
27
27
 
28
28
  import sys
29
29
  import os
30
- import argparse
31
30
  import numpy as np
32
31
  import rasterio
33
32
  from rasterio.transform import Affine
34
33
  from PySide6.QtWidgets import (
35
34
  QApplication, QMainWindow, QGraphicsView, QGraphicsScene, QGraphicsPixmapItem,
36
- QScrollBar, QGraphicsPathItem, QVBoxLayout, QHBoxLayout, QWidget, QStatusBar
35
+ QScrollBar, QGraphicsPathItem, QVBoxLayout, QWidget, QStatusBar
37
36
  )
38
37
  from PySide6.QtGui import QImage, QPixmap, QPainter, QPen, QColor, QPainterPath
39
38
  from PySide6.QtCore import Qt
@@ -41,8 +40,10 @@ from PySide6.QtCore import Qt
41
40
  import matplotlib.cm as cm
42
41
  import warnings
43
42
  warnings.filterwarnings("ignore", category=RuntimeWarning, module="shapely")
43
+ warnings.filterwarnings("ignore", category=UserWarning, module="rasterio")
44
+ warnings.filterwarnings("ignore", category=UserWarning, module="xarray")
44
45
 
45
- __version__ = "0.2.5"
46
+ __version__ = "0.2.6"
46
47
 
47
48
  # Optional overlay deps
48
49
  try:
@@ -166,9 +167,13 @@ class TiffViewer(QMainWindow):
166
167
  rgb: list[int] | None = None,
167
168
  rgbfiles: list[str] | None = None,
168
169
  shapefiles: list[str] | None = None,
169
- shp_color: str = "white",
170
+ shp_color: str = "cyan",
170
171
  shp_width: float = 2,
171
172
  subset: int | None = None,
173
+ vmin: float | None = None,
174
+ vmax: float | None = None,
175
+ cartopy: str = "on",
176
+ timestep: int | None = None,
172
177
  ):
173
178
  super().__init__()
174
179
 
@@ -177,6 +182,13 @@ class TiffViewer(QMainWindow):
177
182
  self.band = int(band)
178
183
  self.rgb = rgb
179
184
  self.rgbfiles = rgbfiles
185
+ self._user_vmin = vmin
186
+ self._user_vmax = vmax
187
+ self.cartopy_mode = cartopy.lower()
188
+
189
+ if not tif_path and not rgbfiles:
190
+ print("Usage: viewtif <file.tif>")
191
+ sys.exit(1)
180
192
 
181
193
  self._scale_arg = max(1, int(scale or 1))
182
194
  self._transform: Affine | None = None
@@ -206,41 +218,53 @@ class TiffViewer(QMainWindow):
206
218
  self.data = arr
207
219
  self.band_count = 3
208
220
  self.rgb = [os.path.basename(red), os.path.basename(green), os.path.basename(blue)]
209
- # Use common prefix for title if tif_path not passed
210
221
  self.tif_path = self.tif_path or (os.path.commonprefix([red, green, blue]) or red)
211
222
 
212
223
  elif tif_path:
213
- # ---------------- Handle File Geodatabase (.gdb) ---------------- #
214
- if tif_path and tif_path.lower().endswith(".gdb") and ":" not in tif_path:
224
+
225
+ # ---------------- Handle File Geodatabase (.gdb) ---------------- #
226
+ if tif_path.lower().endswith(".gdb") and ":" not in tif_path:
227
+
215
228
  import re, subprocess
216
- gdb_path = tif_path # use full path to .gdb
229
+ gdb_path = tif_path
230
+
217
231
  try:
218
- out = subprocess.check_output(["gdalinfo", "-norat", gdb_path], text=True)
232
+ out = subprocess.check_output(
233
+ ["gdalinfo", "-norat", gdb_path],
234
+ text=True
235
+ )
219
236
  rasters = re.findall(r"RASTER_DATASET=(\S+)", out)
237
+
220
238
  if not rasters:
221
239
  print(f"[WARN] No raster datasets found in {os.path.basename(gdb_path)}.")
222
240
  sys.exit(0)
223
- else:
224
- print(f"Found {len(rasters)} raster dataset{'s' if len(rasters) > 1 else ''}:")
225
- for i, r in enumerate(rasters):
226
- print(f"[{i}] {r}")
227
- print("\nUse one of these names to open. For example, to open the first raster:")
228
- print(f'viewtif "OpenFileGDB:{gdb_path}:{rasters[0]}"')
229
- sys.exit(0)
230
- except subprocess.CalledProcessError as e:
231
- print(f"[WARN] Could not inspect FileGDB: {e}")
241
+
242
+ print(f"Found {len(rasters)} raster dataset{'s' if len(rasters) > 1 else ''}:")
243
+ for i, r in enumerate(rasters):
244
+ print(f"[{i}] {r}")
245
+
246
+ print("\nUse one of these names to open. For example, to open the first raster:")
247
+ print(f'viewtif "OpenFileGDB:{gdb_path}:{rasters[0]}"')
232
248
  sys.exit(0)
233
249
 
234
- # --- Warn for large files before loading ---
250
+ except (subprocess.CalledProcessError, FileNotFoundError) as e:
251
+ print("[ERROR] This file requires full GDAL support.")
252
+ sys.exit(1)
253
+
254
+ # Warn for large files
235
255
  warn_if_large(tif_path, scale=self._scale_arg)
236
256
 
237
- # --------------------- Detect NetCDF --------------------- #
238
- if tif_path and tif_path.lower().endswith((".nc", ".netcdf")):
239
- try:
240
- # Lazy-load NetCDF dependencies
241
- import xarray as xr
242
- import pandas as pd
243
-
257
+ # ---------------------------------------------------------------
258
+ # Detect NetCDF
259
+ # ---------------------------------------------------------------
260
+ if tif_path.lower().endswith((".nc", ".netcdf")):
261
+ try:
262
+ import xarray as xr
263
+ except ModuleNotFoundError:
264
+ print("NetCDF support requires extra dependencies.")
265
+ print("Install them with: pip install viewtif[netcdf]")
266
+ sys.exit(0)
267
+
244
268
  # Open the NetCDF file
245
269
  ds = xr.open_dataset(tif_path)
246
270
 
@@ -270,11 +294,11 @@ class TiffViewer(QMainWindow):
270
294
 
271
295
  # Get coordinate info if available
272
296
  self._has_geo_coords = False
273
- if 'lon' in ds.coords and 'lat' in ds.coords:
297
+ if "lon" in ds.coords and "lat" in ds.coords:
274
298
  self._has_geo_coords = True
275
299
  self._lon_data = ds.lon.values
276
300
  self._lat_data = ds.lat.values
277
- elif 'longitude' in ds.coords and 'latitude' in ds.coords:
301
+ elif "longitude" in ds.coords and "latitude" in ds.coords:
278
302
  self._has_geo_coords = True
279
303
  self._lon_data = ds.longitude.values
280
304
  self._lat_data = ds.latitude.values
@@ -282,29 +306,18 @@ class TiffViewer(QMainWindow):
282
306
  # Handle time or other index dimension if present
283
307
  self._has_time_dim = False
284
308
  self._time_dim_name = None
285
- time_index = 0
286
309
 
287
310
  # Look for a time dimension first
288
311
  if 'time' in var_data.dims:
289
312
  self._has_time_dim = True
290
- self._time_dim_name = 'time'
291
- self._time_values = ds['time'].values
313
+ self._time_dim_name = "time"
314
+ self._time_values = ds["time"].values
292
315
  self._time_index = 0
293
316
  print(f"NetCDF time dimension detected: {len(self._time_values)} steps")
294
-
295
- self.band_count = var_data.sizes['time']
317
+ self.band_count = var_data.sizes["time"]
296
318
  self.band_index = 0
297
- self._time_dim_name = 'time'
319
+ var_data = var_data.isel(time=0)
298
320
 
299
- # Try to format time values for better display
300
- time_units = getattr(ds.time, 'units', None)
301
- time_calendar = getattr(ds.time, 'calendar', 'standard')
302
-
303
- # Select first time step by default
304
- var_data = var_data.isel(time=time_index)
305
-
306
- # If no time dimension but variable has multiple dimensions,
307
- # use the first non-spatial dimension as a "time" dimension
308
321
  elif len(var_data.dims) > 2:
309
322
  # Try to find a dimension that's not lat/lon
310
323
  spatial_dims = ['lat', 'lon', 'latitude', 'longitude', 'y', 'x']
@@ -313,91 +326,73 @@ class TiffViewer(QMainWindow):
313
326
  self._has_time_dim = True
314
327
  self._time_dim_name = dim
315
328
  self._time_values = ds[dim].values
316
- self._time_index = time_index
317
-
318
- # Select first index by default
319
- var_data = var_data.isel({dim: time_index})
329
+ self._time_index = 0
330
+ var_data = var_data.isel({dim: 0})
320
331
  break
321
-
322
- # Convert to numpy array
332
+
323
333
  arr = var_data.values.astype(np.float32)
324
-
325
- # Process array based on dimensions
326
- if arr.ndim > 2:
327
- # Keep only lat/lon dimensions for 3D+ arrays
328
- arr = np.squeeze(arr)
329
-
330
- # --- Downsample large arrays for responsiveness ---
334
+ arr = np.squeeze(arr)
335
+
336
+ # --------------------------------------------------------
337
+ # Apply timestep jump after base array is created
338
+ # --------------------------------------------------------
339
+ if timestep is not None and self._has_time_dim:
340
+ ts = max(1, min(timestep, self.band_count))
341
+ self.band_index = ts - 1
342
+ print(f"[INFO] Jumping to timestep {ts}/{self.band_count}")
343
+
344
+ # Replace arr with the correct slice
345
+ frame = self._nc_var_data.isel({self._time_dim_name: self.band_index})
346
+ arr = np.squeeze(frame.values.astype(np.float32))
347
+
331
348
  if arr.ndim >= 2:
332
349
  h, w = arr.shape[:2]
333
350
  if h * w > 4_000_000:
334
351
  step = max(2, int((h * w / 4_000_000) ** 0.5))
335
- if arr.ndim == 2:
336
- arr = arr[::step, ::step]
337
- else:
338
- arr = arr[::step, ::step, :]
339
-
340
- # --- Final assignments ---
352
+ arr = arr[::step, ::step]
353
+
341
354
  self.data = arr
342
355
 
343
356
  # Try to extract CRS from CF conventions
344
357
  self._transform = None
345
358
  self._crs = None
346
- if 'crs' in ds.variables:
359
+
360
+ if "crs" in ds.variables:
347
361
  try:
348
362
  import rasterio.crs
349
- crs_var = ds.variables['crs']
350
- if hasattr(crs_var, 'spatial_ref'):
363
+ crs_var = ds.variables["crs"]
364
+ if hasattr(crs_var, "spatial_ref"):
351
365
  self._crs = rasterio.crs.CRS.from_wkt(crs_var.spatial_ref)
352
366
  except Exception as e:
353
367
  print(f"Could not parse CRS: {e}")
354
-
355
- # Set band info
356
- if arr.ndim == 3:
357
- self.band_count = arr.shape[2]
358
- else:
368
+
369
+ # Preserve time dimension if detected earlier
370
+ if not self._has_time_dim:
359
371
  self.band_count = 1
360
-
361
- self.band_index = 0
372
+ self.band_index = 0
373
+
362
374
  self.vmin, self.vmax = np.nanmin(arr), np.nanmax(arr)
363
-
364
- # --- If user specified --band, start there ---
365
- if self.band and self.band <= self.band_count:
366
- self.band_index = self.band - 1
367
-
368
- # Enable cartopy visualization if available
375
+
376
+ if self._user_vmin is not None:
377
+ self.vmin = self._user_vmin
378
+ if self._user_vmax is not None:
379
+ self.vmax = self._user_vmax
380
+
369
381
  self._use_cartopy = HAVE_CARTOPY and self._has_geo_coords
370
-
371
- except ImportError as e:
372
- if "xarray" in str(e) or "netCDF4" in str(e):
373
- raise RuntimeError(
374
- f"NetCDF support requires additional dependencies.\n"
375
- f"Install them with: pip install viewtif[netcdf]\n"
376
- f"Original error: {str(e)}"
377
- )
378
- else:
379
- raise RuntimeError(f"Error reading NetCDF file: {str(e)}")
380
- except Exception as e:
381
- raise RuntimeError(f"Error reading NetCDF file: {str(e)}")
382
-
383
382
 
384
- # # --- Universal size check before loading ---
385
- # warn_if_large(tif_path, scale=self._scale_arg)
386
-
387
- if False: # Placeholder for previous if condition
388
- pass
389
- # --------------------- Detect HDF/HDF5 --------------------- #
390
- elif tif_path and tif_path.lower().endswith((".hdf", ".h5", ".hdf5")):
383
+ # ---------------------------------------------------------------
384
+ # Detect HDF or HDF5
385
+ # ---------------------------------------------------------------
386
+ elif tif_path.lower().endswith((".hdf", ".h5", ".hdf5")):
391
387
  try:
392
- # Try GDAL first (best support for HDF subdatasets)
393
388
  from osgeo import gdal
394
- gdal.UseExceptions()
389
+ # gdal.UseExceptions()
395
390
 
396
391
  ds = gdal.Open(tif_path)
397
392
  subs = ds.GetSubDatasets()
398
393
 
399
394
  if not subs:
400
- raise ValueError("No subdatasets found in HDF/HDF5 file.")
395
+ raise ValueError("No subdatasets found in HDF file.")
401
396
 
402
397
  # Only list subsets if --subset not given
403
398
  if subset is None:
@@ -409,18 +404,15 @@ class TiffViewer(QMainWindow):
409
404
 
410
405
  # Validate subset index
411
406
  if subset < 0 or subset >= len(subs):
412
- raise ValueError(f"Invalid subset index {subset}. Valid range: 0–{len(subs)-1}")
407
+ raise ValueError(f"Invalid subset index {subset}.")
413
408
 
414
409
  sub_name, desc = subs[subset]
415
410
  print(f"\nOpening subdataset [{subset}]: {desc}")
416
411
  sub_ds = gdal.Open(sub_name)
417
412
 
418
- # --- Read once ---
419
413
  arr = sub_ds.ReadAsArray().astype(np.float32)
420
- #print(f"Raw array shape from GDAL: {arr.shape} (ndim={arr.ndim})")
421
-
422
- # --- Normalize shape ---
423
414
  arr = np.squeeze(arr)
415
+
424
416
  if arr.ndim == 3:
425
417
  # Convert from (bands, rows, cols) → (rows, cols, bands)
426
418
  arr = np.transpose(arr, (1, 2, 0))
@@ -436,93 +428,46 @@ class TiffViewer(QMainWindow):
436
428
  step = max(2, int((h * w / 4_000_000) ** 0.5))
437
429
  arr = arr[::step, ::step] if arr.ndim == 2 else arr[::step, ::step, :]
438
430
 
439
- # --- Final assignments ---
440
431
  self.data = arr
441
432
  self._transform = None
442
433
  self._crs = None
443
434
  self.band_count = arr.shape[2] if arr.ndim == 3 else 1
444
435
  self.band_index = 0
445
436
  self.vmin, self.vmax = np.nanmin(arr), np.nanmax(arr)
437
+ if getattr(self, "_scale_arg", 1) > 1:
438
+ print(f"[INFO] Value range (scaled): {self.vmin:.3f} -> {self.vmax:.3f}")
439
+ else:
440
+ print(f"[INFO] Value range: {self.vmin:.3f} -> {self.vmax:.3f}")
446
441
 
447
- if self.band_count > 1:
448
- print(f"This subdataset has {self.band_count} bands — switch with [ and ] keys.")
442
+ except ImportError as e:
443
+ if "osgeo" in str(e):
444
+ print("[ERROR] This file requires full GDAL support.")
445
+ # print("Install GDAL with:")
446
+ # print(" conda install -c conda-forge gdal")
447
+ sys.exit(1)
449
448
  else:
450
- print("This subdataset has 1 band.")
449
+ print(f"Error reading HDF file: {e}")
450
+ sys.exit(1)
451
451
 
452
- if self.band and self.band <= self.band_count:
453
- self.band_index = self.band - 1
454
- print(f"Opening band {self.band}/{self.band_count}")
452
+ except Exception as e:
453
+ print(f"Error reading HDF file: {e}")
454
+ sys.exit(1)
455
455
 
456
- except ImportError:
457
- # GDAL not available, try rasterio as fallback for NetCDF
458
- print("[INFO] GDAL not available, attempting to read HDF/NetCDF with rasterio...")
459
- try:
460
- import rasterio as rio
461
- with rio.open(tif_path) as src:
462
- print(f"[INFO] NetCDF file opened via rasterio")
463
- print(f"[INFO] Data shape: {src.height} x {src.width} x {src.count} bands")
464
-
465
- if src.count == 0:
466
- raise ValueError("No bands found in NetCDF file.")
467
-
468
- # Determine which band(s) to read
469
- if self.band and self.band <= src.count:
470
- band_indices = [self.band]
471
- print(f"Opening band {self.band}/{src.count}")
472
- elif rgb and all(b <= src.count for b in rgb):
473
- band_indices = rgb
474
- print(f"Opening bands {rgb} as RGB")
475
- else:
476
- band_indices = list(range(1, min(src.count + 1, 4))) # Read up to 3 bands
477
- print(f"Opening bands {band_indices}")
478
-
479
- # Read selected bands
480
- bands = []
481
- for b in band_indices:
482
- band_data = src.read(b, out_shape=(src.height // self._scale_arg, src.width // self._scale_arg))
483
- bands.append(band_data)
484
-
485
- # Stack into array
486
- arr = np.stack(bands, axis=-1).astype(np.float32) if len(bands) > 1 else bands[0].astype(np.float32)
487
-
488
- # Handle no-data values
489
- nd = src.nodata
490
- if nd is not None:
491
- if arr.ndim == 3:
492
- arr[arr == nd] = np.nan
493
- else:
494
- arr[arr == nd] = np.nan
495
-
496
- # Final assignments
497
- self.data = arr
498
- self._transform = src.transform
499
- self._crs = src.crs
500
- self.band_count = arr.shape[2] if arr.ndim == 3 else 1
501
- self.band_index = 0
502
- self.vmin, self.vmax = np.nanmin(arr), np.nanmax(arr)
503
-
504
- if self.band_count > 1:
505
- print(f"Loaded {self.band_count} bands — switch with [ and ] keys.")
506
- else:
507
- print("Loaded 1 band.")
508
- except Exception as e:
509
- raise RuntimeError(
510
- f"Failed to read HDF/NetCDF file: {e}\n"
511
- "For full HDF support, install GDAL: pip install GDAL"
512
- )
513
-
514
- # --------------------- Regular GeoTIFF --------------------- #
456
+ # ---------------------------------------------------------------
457
+ # Regular TIFF
458
+ # ---------------------------------------------------------------
515
459
  else:
516
- if tif_path and os.path.dirname(tif_path).endswith(".gdb"):
517
- tif_path = f"OpenFileGDB:{os.path.dirname(tif_path)}:{os.path.basename(tif_path)}"
518
-
519
460
  import rasterio as rio_module
520
461
  with rio_module.open(tif_path) as src:
521
462
  self._transform = src.transform
522
463
  self._crs = src.crs
464
+
523
465
  if rgb is not None:
524
- bands = [src.read(b, out_shape=(src.height // self._scale_arg, src.width // self._scale_arg))
525
- for b in rgb]
466
+ bands = [
467
+ src.read(b, out_shape=(src.height // self._scale_arg, src.width // self._scale_arg))
468
+ for b in rgb
469
+ ]
470
+
526
471
  arr = np.stack(bands, axis=-1).astype(np.float32)
527
472
  nd = src.nodata
528
473
  if nd is not None:
@@ -540,7 +485,14 @@ class TiffViewer(QMainWindow):
540
485
  self.data = arr
541
486
  self.band_count = src.count
542
487
 
543
- # single-band display range (fast stats or fallback)
488
+ if self.band_count == 1:
489
+ print("[INFO] This TIFF has 1 band.")
490
+ else:
491
+ print(
492
+ f"[INFO] This TIFF has {self.band_count} bands. "
493
+ "Use [ and ] to switch bands, or use --rgb R G B."
494
+ )
495
+
544
496
  try:
545
497
  stats = src.stats(self.band)
546
498
  if stats and stats.min is not None and stats.max is not None:
@@ -549,9 +501,10 @@ class TiffViewer(QMainWindow):
549
501
  raise ValueError("No stats in file")
550
502
  except Exception:
551
503
  self.vmin, self.vmax = np.nanmin(arr), np.nanmax(arr)
552
-
553
- else:
554
- raise ValueError("Provide a TIFF path or --rgbfiles.")
504
+ if getattr(self, "_scale_arg", 1) > 1:
505
+ print(f"[INFO] Value range (scaled): {self.vmin:.3f} -> {self.vmax:.3f}")
506
+ else:
507
+ print(f"[INFO] Value range: {self.vmin:.3f} -> {self.vmax:.3f}")
555
508
 
556
509
  # Window title
557
510
  self.update_title()
@@ -561,7 +514,6 @@ class TiffViewer(QMainWindow):
561
514
  self.gamma = 1.0
562
515
 
563
516
  # Colormap (single-band)
564
- # For NetCDF temperature data, have three colormaps in rotation
565
517
  if tif_path and tif_path.lower().endswith(('.nc', '.netcdf')):
566
518
  self.cmap_names = ["RdBu_r", "viridis", "magma"] # three colormaps for NetCDF
567
519
  self.cmap_index = 0 # start with RdBu_r
@@ -594,7 +546,9 @@ class TiffViewer(QMainWindow):
594
546
  self._last_rgb = None
595
547
 
596
548
  # --- Initial render ---
549
+ self._suppress_scale_print = True # Need for NetCDF
597
550
  self.update_pixmap()
551
+ self._suppress_scale_print = False # Need for NetCDF
598
552
 
599
553
  # Overlays (if any)
600
554
  if self._shapefiles:
@@ -605,9 +559,32 @@ class TiffViewer(QMainWindow):
605
559
  if self.pixmap_item is not None:
606
560
  rect = self.pixmap_item.boundingRect()
607
561
  self.scene.setSceneRect(rect)
562
+
563
+ # Fit first
608
564
  self.view.fitInView(self.pixmap_item, Qt.AspectRatioMode.KeepAspectRatioByExpanding)
609
- self.view.scale(5, 5)
565
+
566
+ # ----------------------------
567
+ # NetCDF needs a different scaling (appears smaller)
568
+ # ----------------------------
569
+ if hasattr(self, "_nc_var_name"):
570
+ # NetCDF view adjustment
571
+ self.view.scale(11.0, 11.0)
572
+ else:
573
+ # Default behavior for TIFF/HDF imagery
574
+ self.view.scale(7.0, 7.0)
575
+
610
576
  self.view.centerOn(self.pixmap_item)
577
+
578
+ # Previous version below
579
+ # # --- Initial render ---
580
+ # self.update_pixmap()
581
+ # self.resize(1200, 800)
582
+ # if self.pixmap_item is not None:
583
+ # rect = self.pixmap_item.boundingRect()
584
+ # self.scene.setSceneRect(rect)
585
+ # self.view.fitInView(self.pixmap_item, Qt.AspectRatioMode.KeepAspectRatioByExpanding)
586
+ # self.view.scale(5, 5)
587
+ # self.view.centerOn(self.pixmap_item)
611
588
 
612
589
  # ---------------------------- Overlays ---------------------------- #
613
590
  def _geo_to_pixel(self, x: float, y: float):
@@ -731,23 +708,40 @@ class TiffViewer(QMainWindow):
731
708
 
732
709
  # ----------------------- Title / Rendering ----------------------- #
733
710
  def update_title(self):
734
- """Show correct title for GeoTIFF or NetCDF time series."""
711
+ """Add band before the title."""
735
712
  import os
713
+ file_name = os.path.basename(self.tif_path)
736
714
 
737
715
  if hasattr(self, "_has_time_dim") and self._has_time_dim:
738
- nc_name = getattr(self, "_nc_var_name", "")
739
- file_name = os.path.basename(self.tif_path)
716
+ # nc_name = getattr(self, "_nc_var_name", "")
717
+
740
718
  title = f"Time step {self.band_index + 1}/{self.band_count} — {file_name}"
719
+
741
720
 
742
721
  elif hasattr(self, "band_index"):
743
- title = f"Band {self.band_index + 1}/{self.band_count} — {os.path.basename(self.tif_path)}"
722
+ title = f"Band {self.band_index + 1}/{self.band_count} — {file_name}"
723
+
724
+ elif self.rgb_mode:
725
+
726
+ # Case 1: --rgbfiles → filenames
727
+ if self.rgbfiles:
728
+ files = [os.path.basename(p) for p in self.rgbfiles]
729
+ title = f"RGB ({files[0]}, {files[1]}, {files[2]})"
730
+
731
+ # Case 2: --rgb → band numbers
732
+ elif self.rgb:
733
+ r, g, b = self.rgb
734
+ title = f"RGB ({r}, {g}, {b}) — {file_name}"
735
+
736
+ else:
737
+ title = f"RGB — {file_name}"
744
738
 
745
- elif self.rgb_mode and self.rgb:
746
- # title = f"RGB {self.rgb} — {os.path.basename(self.tif_path)}"
747
- title = f"RGB {self.rgb}"
739
+ elif not self.rgb_mode:
740
+ # TIFF uses self.band
741
+ title = f"Band {self.band}/{self.band_count} — {file_name}"
748
742
 
749
743
  else:
750
- title = os.path.basename(self.tif_path)
744
+ title = {file_name}
751
745
 
752
746
  print(f"Title: {title}")
753
747
  self.setWindowTitle(title)
@@ -794,7 +788,8 @@ class TiffViewer(QMainWindow):
794
788
  return frame
795
789
 
796
790
  step = int(self._scale_arg)
797
- print(f"Applying scale factor {step} to current frame")
791
+ if not hasattr(self, "_suppress_scale_print"):
792
+ print(f"Applying scale factor {self._scale_arg} to current frame")
798
793
 
799
794
  # Downsample the frame
800
795
  frame = frame[::step, ::step]
@@ -829,10 +824,6 @@ class TiffViewer(QMainWindow):
829
824
  if hasattr(frame, "values"):
830
825
  frame = frame.values
831
826
 
832
- # Apply same scaling factor (if any)
833
- if hasattr(self, "_scale_arg") and self._scale_arg > 1:
834
- step = int(self._scale_arg)
835
-
836
827
  return frame.astype(np.float32)
837
828
 
838
829
  def format_time_value(self, time_value):
@@ -888,7 +879,7 @@ class TiffViewer(QMainWindow):
888
879
  return rgb
889
880
 
890
881
  def _render_cartopy_map(self, data):
891
- """Render a NetCDF variable with cartopy for better geographic visualization"""
882
+ """ Use cartopy for better visualization"""
892
883
  import matplotlib.pyplot as plt
893
884
  from matplotlib.backends.backend_agg import FigureCanvasAgg
894
885
  import cartopy.crs as ccrs
@@ -903,7 +894,6 @@ class TiffViewer(QMainWindow):
903
894
  lats = self._lat_data
904
895
 
905
896
  # Create contour plot
906
- levels = 20
907
897
  if hasattr(plt.cm, self.cmap_name):
908
898
  cmap = getattr(plt.cm, self.cmap_name)
909
899
  else:
@@ -922,8 +912,6 @@ class TiffViewer(QMainWindow):
922
912
  norm_data = np.power(norm_data, self.gamma)
923
913
  norm_data = norm_data * rng + vmin
924
914
 
925
- # Downsample coordinates to match downsampled data shape
926
- # --- Align coordinates with data shape (no stepping assumptions) ---
927
915
  # Downsample coordinates to match downsampled data shape
928
916
  data_height, data_width = data.shape[:2]
929
917
  lat_samples = len(lats)
@@ -946,27 +934,37 @@ class TiffViewer(QMainWindow):
946
934
  # print("[DEBUG] 2D lat grid ascending → flip lats_downsampled vertically")
947
935
  lats_downsampled = np.flipud(lats_downsampled)
948
936
 
949
- # Convert 0–360 longitude to −180–180 if needed
950
- if lons_downsampled.max() > 180:
951
- lons_downsampled = ((lons_downsampled + 180) % 360) - 180
937
+ # ---- Fix longitude and sort correctly ----
938
+ lons_ds = lons_downsampled.copy()
952
939
 
940
+ # Convert 0–360 → -180–180 only once
941
+ if lons_ds.max() > 180:
942
+ lons_ds = ((lons_ds + 180) % 360) - 180
953
943
 
954
- # --- Build meshgrid AFTER any flip ---
955
- lon_grid, lat_grid = np.meshgrid(lons_downsampled, lats_downsampled, indexing="xy")
944
+ # Sort and reorder data
945
+ sort_idx = np.argsort(lons_ds)
946
+ lons_ds = lons_ds[sort_idx]
947
+ data = data[:, sort_idx]
956
948
 
957
- # Use pcolormesh (more stable than contourf for gridded data)
958
- img = ax.pcolormesh(
959
- lon_grid, lat_grid, data,
960
- transform=ccrs.PlateCarree(),
961
- cmap=cmap,
962
- shading="auto"
949
+ extent = (
950
+ float(lons_ds[0]),
951
+ float(lons_ds[-1]),
952
+ float(lats_downsampled[-1]),
953
+ float(lats_downsampled[0])
963
954
  )
964
955
 
965
- # Set extent from the 1D vectors (already flipped if needed)
966
- ax.set_extent(
967
- [lons_downsampled.min(), lons_downsampled.max(),
968
- lats_downsampled.min(), lats_downsampled.max()],
969
- crs=ccrs.PlateCarree()
956
+ vmin = self.vmin if self._user_vmin is not None else np.nanmin(data)
957
+ vmax = self.vmax if self._user_vmax is not None else np.nanmax(data)
958
+
959
+ # Changed from pcolormesh to imshow to prevent artefacts when used with cartopy
960
+ img = ax.imshow(
961
+ data,
962
+ extent=extent,
963
+ transform=ccrs.PlateCarree(),
964
+ cmap=cmap,
965
+ interpolation="nearest",
966
+ vmin=vmin,
967
+ vmax=vmax
970
968
  )
971
969
 
972
970
  # Add map features
@@ -1004,43 +1002,65 @@ class TiffViewer(QMainWindow):
1004
1002
 
1005
1003
  # Close figure to prevent memory leak
1006
1004
  plt.close(fig)
1005
+ del fig
1007
1006
 
1008
1007
  return rgb
1009
-
1008
+
1010
1009
  def update_pixmap(self):
1011
- # --- Select display data ---
1012
- if hasattr(self, "band_index"):
1013
- # HDF or scientific multi-band
1014
- if self.data.ndim == 3:
1015
- a = self.data[:, :, self.band_index]
1016
- else:
1017
- a = self.data
1018
- rgb = None
1010
+ # ------------------------------------------------------------------
1011
+ # Select respective data (a = single-band 2D, rgb = RGB array)
1012
+ # ------------------------------------------------------------------
1013
+
1014
+ rgb = None # ensure defined
1015
+
1016
+ # Case 1: RGB override (GeoTIFF or RGB-files)
1017
+ if self.rgb_mode:
1018
+ rgb = self.data
1019
+ a = None
1020
+
1021
+ # Case 2: Scientific multi-band (NetCDF/HDF)
1022
+ elif hasattr(self, "band_index"):
1023
+ # Always get consistent per-frame 2D data
1024
+ a = self.get_current_frame()
1025
+
1026
+ # Case 3: Regular GeoTIFF single-band
1019
1027
  else:
1020
- # Regular GeoTIFF (could be RGB or single-band)
1021
- if self.rgb_mode: # user explicitly passed --rgb or --rgbtiles
1022
- rgb = self.data
1023
- a = None
1024
- else:
1025
- a = self.data
1026
- rgb = None
1027
- # ----------------------------
1028
+ rgb = None
1029
+ a = self.data
1028
1030
 
1029
1031
  # --- Render image ---
1030
- # Check if we should use cartopy for NetCDF visualization
1032
+ # Cartopy is only relevant for NetCDF
1031
1033
  use_cartopy = False
1032
- if hasattr(self, '_use_cartopy') and self._use_cartopy and HAVE_CARTOPY:
1033
- if hasattr(self, '_has_geo_coords') and self._has_geo_coords:
1034
- use_cartopy = True
1035
-
1034
+
1035
+ if hasattr(self, "_nc_var_name"):
1036
+ use_cartopy = (
1037
+ self.cartopy_mode == "on"
1038
+ and HAVE_CARTOPY
1039
+ and getattr(self, "_use_cartopy", False)
1040
+ and getattr(self, "_has_geo_coords", False)
1041
+ )
1042
+
1043
+ # Inform user when cartopy was requested but cannot be used
1044
+ if self.cartopy_mode == "on" and not use_cartopy:
1045
+ if not HAVE_CARTOPY:
1046
+ print("[INFO] Cartopy not installed — using standard scientific rendering.")
1047
+ elif not getattr(self, "_use_cartopy", False):
1048
+ print("[INFO] This file lacks geospatial coordinates — cartopy disabled.")
1049
+ elif not getattr(self, "_has_geo_coords", False):
1050
+ print("[INFO] No lat/lon coordinates found — cartopy disabled.")
1051
+
1036
1052
  if use_cartopy:
1037
- # Render with cartopy for better geographic visualization
1038
1053
  rgb = self._render_cartopy_map(a)
1039
1054
  elif rgb is None:
1040
- # Standard grayscale rendering for single-band (scientific) data
1055
+ # Standard grayscale rendering for single-band data
1041
1056
  finite = np.isfinite(a)
1042
- vmin, vmax = np.nanmin(a), np.nanmax(a)
1057
+
1058
+ # Respect user-specified limits
1059
+ vmin = self._user_vmin if self._user_vmin is not None else np.nanmin(a)
1060
+ vmax = self._user_vmax if self._user_vmax is not None else np.nanmax(a)
1061
+
1043
1062
  rng = max(vmax - vmin, 1e-12)
1063
+
1044
1064
  norm = np.zeros_like(a, dtype=np.float32)
1045
1065
  if np.any(finite):
1046
1066
  norm[finite] = (a[finite] - vmin) / rng
@@ -1051,13 +1071,15 @@ class TiffViewer(QMainWindow):
1051
1071
  else:
1052
1072
  # True RGB mode (unchanged)
1053
1073
  rgb = self._render_rgb()
1054
- # ----------------------
1055
1074
 
1056
1075
  h, w, _ = rgb.shape
1057
1076
  self._last_rgb = rgb
1077
+
1058
1078
  qimg = QImage(rgb.data, w, h, 3 * w, QImage.Format.Format_RGB888)
1059
1079
  pix = QPixmap.fromImage(qimg)
1080
+
1060
1081
  if self.pixmap_item is None:
1082
+
1061
1083
  self.pixmap_item = QGraphicsPixmapItem(pix)
1062
1084
  self.pixmap_item.setZValue(0.0)
1063
1085
  self.scene.addItem(self.pixmap_item)
@@ -1082,6 +1104,7 @@ class TiffViewer(QMainWindow):
1082
1104
  if nd is not None:
1083
1105
  arr[arr == nd] = np.nan
1084
1106
  self.data = arr
1107
+
1085
1108
  self.vmin, self.vmax = np.nanmin(arr), np.nanmax(arr)
1086
1109
  self.update_pixmap()
1087
1110
  self.update_title()
@@ -1126,6 +1149,7 @@ class TiffViewer(QMainWindow):
1126
1149
  # For other files, toggle between two colormaps
1127
1150
  else:
1128
1151
  self.cmap_name, self.alt_cmap_name = self.alt_cmap_name, self.cmap_name
1152
+ print(f"Colormap: {self.cmap_name}")
1129
1153
  self.update_pixmap()
1130
1154
 
1131
1155
  # Band switch
@@ -1186,12 +1210,14 @@ def run_viewer(
1186
1210
  shapefile=None,
1187
1211
  shp_color=None,
1188
1212
  shp_width=None,
1189
- subset=None
1213
+ subset=None,
1214
+ vmin=None,
1215
+ vmax=None,
1216
+ cartopy="on",
1217
+ timestep=None
1190
1218
  ):
1191
1219
 
1192
1220
  """Launch the TiffViewer app"""
1193
- from PySide6.QtCore import Qt
1194
- # QApplication.setAttribute(Qt.ApplicationAttribute.AA_EnableHighDpiScaling, True)
1195
1221
  app = QApplication(sys.argv)
1196
1222
  win = TiffViewer(
1197
1223
  tif_path,
@@ -1202,7 +1228,11 @@ def run_viewer(
1202
1228
  shapefiles=shapefile,
1203
1229
  shp_color=shp_color,
1204
1230
  shp_width=shp_width,
1205
- subset=subset
1231
+ subset=subset,
1232
+ vmin=vmin,
1233
+ vmax=vmax,
1234
+ cartopy=cartopy,
1235
+ timestep=timestep,
1206
1236
  )
1207
1237
  win.show()
1208
1238
  sys.exit(app.exec())
@@ -1210,17 +1240,34 @@ def run_viewer(
1210
1240
  import click
1211
1241
 
1212
1242
  @click.command()
1213
- @click.version_option("0.2.5", prog_name="viewtif")
1243
+ @click.version_option(__version__, prog_name="viewtif")
1214
1244
  @click.argument("tif_path", required=False)
1215
1245
  @click.option("--band", default=1, show_default=True, type=int, help="Band number to display")
1216
1246
  @click.option("--scale", default=1.0, show_default=True, type=int, help="Scale factor for display")
1217
1247
  @click.option("--rgb", nargs=3, type=int, help="Three band numbers for RGB, e.g. --rgb 4 3 2")
1218
1248
  @click.option("--rgbfiles", nargs=3, type=str, help="Three single-band TIFFs for RGB, e.g. --rgbfiles B4.tif B3.tif B2.tif")
1219
1249
  @click.option("--shapefile", multiple=True, type=str, help="One or more shapefiles to overlay")
1220
- @click.option("--shp-color", default="white", show_default=True, help="Overlay color (name or #RRGGBB).")
1250
+ @click.option("--shp-color", default="cyan", show_default=True, help="Overlay color (name or #RRGGBB).")
1221
1251
  @click.option("--shp-width", default=1.0, show_default=True, type=float, help="Overlay line width (screen pixels).")
1222
1252
  @click.option("--subset", default=None, type=int, help="Open specific subdataset index in .hdf/.h5 file or variable in NetCDF file")
1223
- def main(tif_path, band, scale, rgb, rgbfiles, shapefile, shp_color, shp_width, subset):
1253
+ @click.option("--vmin", type=float, default=None, help="Manual minimum display value")
1254
+ @click.option("--vmax", type=float, default=None, help="Manual maximum display value")
1255
+ @click.option(
1256
+ "--timestep",
1257
+ type=int,
1258
+ default=None,
1259
+ help="For NetCDF files, jump directly to a specific time index (1-based)."
1260
+ )
1261
+ @click.option(
1262
+ "--cartopy",
1263
+ type=click.Choice(["on", "off"], case_sensitive=False),
1264
+ default="on",
1265
+ show_default=True,
1266
+ help="Use cartopy for NetCDF geospatial rendering."
1267
+ )
1268
+
1269
+
1270
+ def main(tif_path, band, scale, rgb, rgbfiles, shapefile, shp_color, shp_width, subset, vmin, vmax, cartopy, timestep):
1224
1271
  """Lightweight GeoTIFF, NetCDF, and HDF viewer."""
1225
1272
  # --- Warn early if shapefile requested but geopandas missing ---
1226
1273
  if shapefile and not HAVE_GEO:
@@ -1239,7 +1286,11 @@ def main(tif_path, band, scale, rgb, rgbfiles, shapefile, shp_color, shp_width,
1239
1286
  shapefile=shapefile,
1240
1287
  shp_color=shp_color,
1241
1288
  shp_width=shp_width,
1242
- subset=subset
1289
+ subset=subset,
1290
+ vmin=vmin,
1291
+ vmax=vmax,
1292
+ cartopy=cartopy,
1293
+ timestep=timestep,
1243
1294
  )
1244
1295
 
1245
1296
  if __name__ == "__main__":
@@ -1,13 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: viewtif
3
- Version: 0.2.5
3
+ Version: 0.2.6
4
4
  Summary: Lightweight GeoTIFF, NetCDF, HDF/HDF5, and Esri File Geodatabase (.gdb) viewer with optional shapefile overlay. NetCDF and cartopy support available via pip install viewtif[netcdf].
5
5
  Project-URL: Homepage, https://github.com/nkeikon/tifviewer
6
6
  Project-URL: Source, https://github.com/nkeikon/tifviewer
7
7
  Project-URL: Issues, https://github.com/nkeikon/tifviewer/issues
8
8
  Author: Keiko Nomura
9
9
  License: MIT
10
- Requires-Python: >=3.9
10
+ License-File: LICENSE
11
+ Requires-Python: >=3.10
11
12
  Requires-Dist: click>=8.1
12
13
  Requires-Dist: matplotlib>=3.7
13
14
  Requires-Dist: numpy>=1.23
@@ -26,9 +27,7 @@ Description-Content-Type: text/markdown
26
27
  # viewtif
27
28
  [![Downloads](https://static.pepy.tech/badge/viewtif)](https://pepy.tech/project/viewtif)
28
29
  [![PyPI version](https://img.shields.io/pypi/v/viewtif)](https://pypi.org/project/viewtif/)
29
- [![Python version](https://img.shields.io/badge/python-%3E%3D3.9-blue.svg)](https://pypi.org/project/viewtif/)
30
-
31
-
30
+ [![Python version](https://img.shields.io/badge/python-%3E%3D3.10-blue.svg)](https://pypi.org/project/viewtif/)
32
31
 
33
32
  A lightweight GeoTIFF viewer for quick visualization directly from the command line.
34
33
 
@@ -39,7 +38,7 @@ You can visualize single-band GeoTIFFs, RGB composites, HDF, NetCDF files and sh
39
38
  ```bash
40
39
  pip install viewtif
41
40
  ```
42
- > **Note:** On Linux, you may need python3-tk, libqt5gui5, or PySide6 dependencies.
41
+ > **Note:** Requires Python 3.10 or higher. On Linux, you may need python3-tk, libqt5gui5, or PySide6 dependencies.
43
42
  >
44
43
  >`viewtif` requires a graphical display environment.
45
44
  > It may not run properly on headless systems (e.g., HPC compute nodes or remote servers without X11 forwarding).
@@ -52,19 +51,20 @@ pip install "viewtif[geo]"
52
51
  > **Note:** For macOS(zsh) users:
53
52
  > Make sure to include the quotes, or zsh will interpret it as a pattern.
54
53
 
55
- #### HDF/HDF5 support
54
+ #### NetCDF support
55
+ ```bash
56
+ pip install "viewtif[netcdf]"
57
+ ```
58
+ > **Note:** For enhanced geographic visualization with map projections, coastlines, and borders, install with cartopy: `pip install "viewtif[netcdf]"` (cartopy is included in the netcdf extra). If cartopy is not available, netCDF files will still display with standard rendering.
59
+
60
+ #### HDF/HDF5 & FileGDB support
61
+ GDAL is required to open `.hdf`, .`h5`, `.hdf5`, and `.gdb` files.
56
62
  ```bash
57
63
  brew install gdal # macOS
58
64
  sudo apt install gdal-bin python3-gdal # Linux
59
65
  pip install GDAL
60
66
  ```
61
- > **Note:** GDAL is required to open `.hdf`, .`h5`, and `.hdf5` files. If it’s missing, viewtif will display: `RuntimeError: HDF support requires GDAL.`
62
67
 
63
- #### NetCDF support
64
- ```bash
65
- pip install "viewtif[netcdf]"
66
- ```
67
- > **Note:** For enhanced geographic visualization with map projections, coastlines, and borders, install with cartopy: `pip install "viewtif[netcdf]"` (cartopy is included in the netcdf extra). If cartopy is not available, netCDF files will still display with standard rendering.
68
68
  ## Quick Start
69
69
  ```bash
70
70
  # View a GeoTIFF
@@ -76,9 +76,16 @@ viewtif --rgbfiles \
76
76
  HLS_B03.tif \
77
77
  HLS_B02.tif
78
78
 
79
+ # View an RGB composite from a multi-band file
80
+ viewtif rgb.tif --rgb 4 3 2
81
+
79
82
  # View with shapefile overlay
80
83
  viewtif ECOSTRESS_LST.tif \
81
84
  --shapefile Zip_Codes.shp
85
+
86
+ # Change the color and width
87
+ viewtif ECOSTRESS_LST.tif \
88
+ --shapefile Zip_Codes.shp --shp-color red --shp-width 2
82
89
  ```
83
90
  ### Update in v1.0.6: HDF/HDF5 support
84
91
  `viewtif` can open `.hdf`, `.h5`, and `.hdf5` files that contain multiple subdatasets. When opened, it lists available subdatasets and lets you view one by index. You can also specify a band to display (default is the first band) or change bands interactively with '[' and ']'.
@@ -96,11 +103,7 @@ viewtif AG100.v003.33.-107.0001.h5 --subset 1 --band 3
96
103
  `[WARN] raster lacks CRS/transform; cannot place overlays.`
97
104
 
98
105
  ### Update in v1.0.7: File Geodatabase (.gdb) support
99
- `viewtif` can now open raster datasets stored inside Esri File Geodatabases (`.gdb`). When you open a .gdb directly, `viewtif`` will list available raster datasets first, then you can choose one to view.
100
-
101
- Most Rasterio installations already include the OpenFileGDB driver, so .gdb datasets often open without installing GDAL manually.
102
-
103
- If you encounter: RuntimeError: GDB support requires GDAL, install GDAL as shown above to enable the driver.
106
+ `viewtif` can now open raster datasets stored inside Esri File Geodatabases (`.gdb`). When you open a .gdb directly, `viewtif` will list available raster datasets first, then you can choose one to view.
104
107
 
105
108
  ```bash
106
109
  # List available raster datasets
@@ -117,11 +120,19 @@ If the dataset is very large (e.g., >20 million pixels), it will pause and warn
117
120
  You can proceed manually or rerun with the `--scale` option for a smaller, faster preview.
118
121
 
119
122
  ### Update in v0.2.2: NetCDF support with optional cartopy visualization
120
- `viewtif` now supports NetCDF (`.nc`) files via xarray, with optional cartopy-based geographic visualization. Use`[` / `]` to move forward or backward through time steps.
123
+ `viewtif` now supports NetCDF (`.nc`) files via xarray, with optional cartopy-based geographic visualization. Use `[` / `]` to move forward or backward through time steps, and press M to switch between the three built-in colormaps ("RdBu_r", "viridis", "magma").
124
+
125
+ ### Update in v0.2.6: New user controls for NetCDF visualization
126
+ - `--vmin [value] --vmax [value]` let you set the display range (for all file types).
127
+ - `--timestep [int]` lets you jump directly to a chosen time slice.
128
+ - `cartopy off` let you see raw NetCDF images.
121
129
 
122
130
  #### Examples
123
131
  ```bash
124
132
  viewtif data.nc
133
+ viewtif data.nc --vmin 280 --vmax 320
134
+ viewtif data.nc --timestep 100
135
+ viewtif data.nc --cartopy off
125
136
  ```
126
137
 
127
138
  ## Controls
@@ -131,7 +142,7 @@ viewtif data.nc
131
142
  | Arrow keys or `WASD` | Pan |
132
143
  | `C` / `V` | Increase / decrease contrast |
133
144
  | `G` / `H` | Increase / decrease gamma |
134
- | `M` | Toggle colormap (`viridis` `magma`) |
145
+ | `M` | Toggle colormap. Single-band: viridis/magma. NetCDF: RdBu_r/viridis/magma. |
135
146
  | `[` / `]` | Previous / next band (or time step) |
136
147
  | `R` | Reset view |
137
148
 
@@ -147,6 +158,8 @@ viewtif data.nc
147
158
  - Zip_Codes.shp and associated files
148
159
  - HLS_B04.tif, HLS_B03.tif, HLS_B02.tif (RGB sample)
149
160
  - AG100.v003.33.-107.0001.h5 (HDF5 file)
161
+ - [tasmax_mon_ACCESS-CM2_ssp370_r1i1p1f1_gn_2021.nc](https://data.nas.nasa.gov/nex-dcp30-cmip6/monthly/ACCESS-CM2/ssp370/r1i1p1f1/tasmax/tasmax_mon_ACCESS-CM2_ssp370_r1i1p1f1_gn_2021.nc)
162
+ - [tas_Amon_KIOST-ESM_ssp585_r1i1p1f1_gr1_201501-210012.nc](https://dap.ceda.ac.uk/badc/cmip6/data/CMIP6/ScenarioMIP/KIOST/KIOST-ESM/ssp585/r1i1p1f1/Amon/tas/gr1/v20191106/tas_Amon_KIOST-ESM_ssp585_r1i1p1f1_gr1_201501-210012.nc)
150
163
 
151
164
  ## Credit & License
152
165
  `viewtif` was inspired by the NASA JPL Thermal Viewer — Semi-Automated Georeferencer (GeoViewer v1.12) developed by Jake Longenecker (University of Miami Rosenstiel School of Marine, Atmospheric & Earth Science) while at the NASA Jet Propulsion Laboratory, California Institute of Technology, with inspiration from JPL’s ECOSTRESS geolocation batch workflow by Andrew Alamillo. The original GeoViewer was released under the MIT License (2025) and may be freely adapted with citation.
@@ -154,15 +167,18 @@ viewtif data.nc
154
167
  ## Citation
155
168
  Longenecker, Jake; Lee, Christine; Hulley, Glynn; Cawse-Nicholson, Kerry; Purkis, Sam; Gleason, Art; Otis, Dan; Galdamez, Ileana; Meiseles, Jacquelyn. GeoViewer v1.12: NASA JPL Thermal Viewer—Semi-Automated Georeferencer User Guide & Reference Manual. Jet Propulsion Laboratory, California Institute of Technology, 2025. PDF.
156
169
 
157
- ## License
158
- This project is released under the MIT License.
159
-
160
170
  ## Contributors
161
171
  - [@HarshShinde0](https://github.com/HarshShinde0) — added mouse-wheel and trackpad zoom support; added NetCDF support with [@nkeikon](https://github.com/nkeikon)
162
172
  - [@p-vdp](https://github.com/p-vdp) — added File Geodatabase (.gdb) raster support
163
173
 
174
+ ## License
175
+ This project is released under the MIT License © 2025 Keiko Nomura.
176
+
177
+ If you find this tool useful, please consider supporting or acknowledging the author.
178
+
164
179
  ## Useful links
165
- - https://www.linkedin.com/posts/desmond-lartey_geospatial-analysts-heads-up-check-out-activity-7386336488050925568-G5bN?utm_source=share&utm_medium=member_desktop&rcm=ACoAAAA0INsBVIO1f6nS_NkKqFh4Na1ZpoYo2fc
166
- - https://www.linkedin.com/posts/keiko-nomura-0231891_dont-you-sometimes-just-want-to-see-a-activity-7383409138296528896-sbRM?utm_source=share&utm_medium=member_desktop&rcm=ACoAAAA0INsBVIO1f6nS_NkKqFh4Na1ZpoYo2fc
167
- - https://www.linkedin.com/posts/keiko-nomura-0231891_now-you-can-see-hdf-files-from-the-command-activity-7383963721561399296-F5i0?utm_source=share&utm_medium=member_desktop&rcm=ACoAAAA0INsBVIO1f6nS_NkKqFh4Na1ZpoYo2fc
168
- - https://www.linkedin.com/posts/keiko-nomura-0231891_you-can-now-view-netcdf-files-nc-from-activity-7386189562072670208-3A0V?utm_source=share&utm_medium=member_desktop&rcm=ACoAAAA0INsBVIO1f6nS_NkKqFh4Na1ZpoYo2fc
180
+ - [Demo by User](https://www.linkedin.com/posts/desmond-lartey_geospatial-analysts-heads-up-check-out-activity-7386336488050925568-G5bN?utm_source=share&utm_medium=member_desktop&rcm=ACoAAAA0INsBVIO1f6nS_NkKqFh4Na1ZpoYo2fc)
181
+ - [Demo at the initial release](https://www.linkedin.com/posts/keiko-nomura-0231891_dont-you-sometimes-just-want-to-see-a-activity-7383409138296528896-sbRM?utm_source=share&utm_medium=member_desktop&rcm=ACoAAAA0INsBVIO1f6nS_NkKqFh4Na1ZpoYo2fc)
182
+ - [Demo on HDF files](https://www.linkedin.com/posts/keiko-nomura-0231891_now-you-can-see-hdf-files-from-the-command-activity-7383963721561399296-F5i0?utm_source=share&utm_medium=member_desktop&rcm=ACoAAAA0INsBVIO1f6nS_NkKqFh4Na1ZpoYo2fc)
183
+ - [Demo on NetCDF files](https://www.linkedin.com/posts/keiko-nomura-0231891_you-can-now-view-netcdf-files-nc-from-activity-7386189562072670208-3A0V?utm_source=share&utm_medium=member_desktop&rcm=ACoAAAA0INsBVIO1f6nS_NkKqFh4Na1ZpoYo2fc)
184
+ - [Demo on Esri Geodatabases](https://www.linkedin.com/posts/keiko-nomura-0231891_did-you-know-viewtif-now-supports-esri-geodatabases-activity-7390165568688959488-pvpk?utm_source=share&utm_medium=member_desktop&rcm=ACoAAAA0INsBVIO1f6nS_NkKqFh4Na1ZpoYo2fc)
@@ -0,0 +1,7 @@
1
+ viewtif/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ viewtif/tif_viewer.py,sha256=-vO-62ApjcG7lVVTHB5ggc9qSeB7YmdR1gmfskp-Vzg,51628
3
+ viewtif-0.2.6.dist-info/METADATA,sha256=CJZC9X2CtZjzQTEVg_MKTKMGNS7Vq8hcTUFEQck0Er4,9541
4
+ viewtif-0.2.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
5
+ viewtif-0.2.6.dist-info/entry_points.txt,sha256=NVEjlRyJ7R7hFPOVsZJio3Hl0VqlX7_oVfA7819XvHM,52
6
+ viewtif-0.2.6.dist-info/licenses/LICENSE,sha256=-aRH3Wo6exTLQLNp9yj6HptkUXXtNjdUSzumkyRsK4s,1069
7
+ viewtif-0.2.6.dist-info/RECORD,,
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Keiko Nomura
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1,6 +0,0 @@
1
- viewtif/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- viewtif/tif_viewer.py,sha256=Vj5_t32_G3ME5WYkDATBfi3Rw5WbzIp6VMgi_RNj0O0,52334
3
- viewtif-0.2.5.dist-info/METADATA,sha256=psLk-EhO4Cqdb01o-LgxjD41WfrICuCeYXK0SFWHdE8,8245
4
- viewtif-0.2.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
5
- viewtif-0.2.5.dist-info/entry_points.txt,sha256=NVEjlRyJ7R7hFPOVsZJio3Hl0VqlX7_oVfA7819XvHM,52
6
- viewtif-0.2.5.dist-info/RECORD,,