├── README.md ├── LICENSE └── nimrod.py /README.md: -------------------------------------------------------------------------------- 1 | # MetOffice_NIMROD 2 | 3 | Python module to extract data from UK Met Office Rain Radar NIMROD image files. 4 | 5 | Features: parses NIMROD format image files, displays header data and allows extraction of raster image to an ESRI ASCII (.asc) format file. A bounding box may be specified to clip the image to the area of interest. Can be imported as a 6 | Python module or run directly as a command line script. Originally written in Python 2.7 (see v1.01), but the current version has been converted to run in Python 3.9 without any additional functionality. 7 | 8 | (This module is developed from a basic Python script written for a hydrological 9 | modelling assignment of my [GIS MSc](http://richard-thomas.github.io/GIS_MSc/)). 10 | 11 | Command line usage: 12 | 13 | ```bash 14 | python nimrod.py [-h] [-q] [-x] [-bbox XMIN XMAX YMIN YMAX] [infile] [outfile] 15 | ``` 16 | 17 | Positional Argument | Description 18 | -- | -- 19 | infile | (Uncompressed) NIMROD input filename 20 | outfile | Output raster filename (*.asc) 21 | 22 | Optional Argument | Description 23 | -- | -- 24 | -h, --help | show this help message and exit 25 | -q, --query | Display metadata 26 | -x, --extract | Extract raster file in ASC format 27 | -bbox XMIN XMAX YMIN YMAX | Bounding box to clip raster data to 28 | 29 | Note that any bounding box must be specified in the same units and projection 30 | as the input file. The bounding box does not need to be contained by the input 31 | raster but must intersect it. 32 | 33 | Example command line usage: 34 | 35 | ```bash 36 | python nimrod.py -bbox 279906 285444 283130 290440 37 | -xq 200802252000_nimrod_ng_radar_rainrate_composite_1km_merged_UK_zip 38 | plynlimon_catchments_rainfall.asc 39 | ``` 40 | 41 | Example Python module usage: 42 | 43 | ```python 44 | import nimrod 45 | a = nimrod.Nimrod(open( 46 | '200802252000_nimrod_ng_radar_rainrate_composite_1km_merged_UK_zip', 'rb')) 47 | a.query() 48 | a.extract_asc(open('full_raster.asc', 'w')) 49 | a.apply_bbox(279906, 285444, 283130, 290440) 50 | a.query() 51 | a.extract_asc(open('clipped_raster.asc', 'w')) 52 | ``` 53 | 54 | Notes: 55 | 56 | 1. Valid for v1.7 and v2.6-4 of NIMROD file specification 57 | 2. Assumes image origin is top left (i.e. that header[24] = 0) 58 | 3. Tested on UK and European composite 1km and 5km data, using Python 3.9 in Windows 10 59 | 4. Further details of NIMROD data and software at the [CEDA Archive](https://data.ceda.ac.uk/badc/ukmo-nimrod) website. 60 | 61 | ---- 62 | Copyright (c) 2021 Richard Thomas 63 | 64 | (`Nimrod.__init__()` method based on read_nimrod.py by Charles Kilburn Aug 2008) 65 | 66 | This program is free software: you can redistribute it and/or modify 67 | it under the terms of the [Artistic License 2.0](https://opensource.org/licenses/Artistic-2.0) as published by the Open Source Initiative . 68 | 69 | This program is distributed in the hope that it will be useful, 70 | but WITHOUT ANY WARRANTY; without even the implied warranty of 71 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 72 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The Artistic License 2.0 2 | 3 | Copyright (c) 2014 richard-thomas 4 | 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | This license establishes the terms under which a given free software 11 | Package may be copied, modified, distributed, and/or redistributed. 12 | The intent is that the Copyright Holder maintains some artistic 13 | control over the development of that Package while still keeping the 14 | Package available as open source and free software. 15 | 16 | You are always permitted to make arrangements wholly outside of this 17 | license directly with the Copyright Holder of a given Package. If the 18 | terms of this license do not permit the full use that you propose to 19 | make of the Package, you should contact the Copyright Holder and seek 20 | a different licensing arrangement. 21 | 22 | Definitions 23 | 24 | "Copyright Holder" means the individual(s) or organization(s) 25 | named in the copyright notice for the entire Package. 26 | 27 | "Contributor" means any party that has contributed code or other 28 | material to the Package, in accordance with the Copyright Holder's 29 | procedures. 30 | 31 | "You" and "your" means any person who would like to copy, 32 | distribute, or modify the Package. 33 | 34 | "Package" means the collection of files distributed by the 35 | Copyright Holder, and derivatives of that collection and/or of 36 | those files. A given Package may consist of either the Standard 37 | Version, or a Modified Version. 38 | 39 | "Distribute" means providing a copy of the Package or making it 40 | accessible to anyone else, or in the case of a company or 41 | organization, to others outside of your company or organization. 42 | 43 | "Distributor Fee" means any fee that you charge for Distributing 44 | this Package or providing support for this Package to another 45 | party. It does not mean licensing fees. 46 | 47 | "Standard Version" refers to the Package if it has not been 48 | modified, or has been modified only in ways explicitly requested 49 | by the Copyright Holder. 50 | 51 | "Modified Version" means the Package, if it has been changed, and 52 | such changes were not explicitly requested by the Copyright 53 | Holder. 54 | 55 | "Original License" means this Artistic License as Distributed with 56 | the Standard Version of the Package, in its current version or as 57 | it may be modified by The Perl Foundation in the future. 58 | 59 | "Source" form means the source code, documentation source, and 60 | configuration files for the Package. 61 | 62 | "Compiled" form means the compiled bytecode, object code, binary, 63 | or any other form resulting from mechanical transformation or 64 | translation of the Source form. 65 | 66 | 67 | Permission for Use and Modification Without Distribution 68 | 69 | (1) You are permitted to use the Standard Version and create and use 70 | Modified Versions for any purpose without restriction, provided that 71 | you do not Distribute the Modified Version. 72 | 73 | 74 | Permissions for Redistribution of the Standard Version 75 | 76 | (2) You may Distribute verbatim copies of the Source form of the 77 | Standard Version of this Package in any medium without restriction, 78 | either gratis or for a Distributor Fee, provided that you duplicate 79 | all of the original copyright notices and associated disclaimers. At 80 | your discretion, such verbatim copies may or may not include a 81 | Compiled form of the Package. 82 | 83 | (3) You may apply any bug fixes, portability changes, and other 84 | modifications made available from the Copyright Holder. The resulting 85 | Package will still be considered the Standard Version, and as such 86 | will be subject to the Original License. 87 | 88 | 89 | Distribution of Modified Versions of the Package as Source 90 | 91 | (4) You may Distribute your Modified Version as Source (either gratis 92 | or for a Distributor Fee, and with or without a Compiled form of the 93 | Modified Version) provided that you clearly document how it differs 94 | from the Standard Version, including, but not limited to, documenting 95 | any non-standard features, executables, or modules, and provided that 96 | you do at least ONE of the following: 97 | 98 | (a) make the Modified Version available to the Copyright Holder 99 | of the Standard Version, under the Original License, so that the 100 | Copyright Holder may include your modifications in the Standard 101 | Version. 102 | 103 | (b) ensure that installation of your Modified Version does not 104 | prevent the user installing or running the Standard Version. In 105 | addition, the Modified Version must bear a name that is different 106 | from the name of the Standard Version. 107 | 108 | (c) allow anyone who receives a copy of the Modified Version to 109 | make the Source form of the Modified Version available to others 110 | under 111 | 112 | (i) the Original License or 113 | 114 | (ii) a license that permits the licensee to freely copy, 115 | modify and redistribute the Modified Version using the same 116 | licensing terms that apply to the copy that the licensee 117 | received, and requires that the Source form of the Modified 118 | Version, and of any works derived from it, be made freely 119 | available in that license fees are prohibited but Distributor 120 | Fees are allowed. 121 | 122 | 123 | Distribution of Compiled Forms of the Standard Version 124 | or Modified Versions without the Source 125 | 126 | (5) You may Distribute Compiled forms of the Standard Version without 127 | the Source, provided that you include complete instructions on how to 128 | get the Source of the Standard Version. Such instructions must be 129 | valid at the time of your distribution. If these instructions, at any 130 | time while you are carrying out such distribution, become invalid, you 131 | must provide new instructions on demand or cease further distribution. 132 | If you provide valid instructions or cease distribution within thirty 133 | days after you become aware that the instructions are invalid, then 134 | you do not forfeit any of your rights under this license. 135 | 136 | (6) You may Distribute a Modified Version in Compiled form without 137 | the Source, provided that you comply with Section 4 with respect to 138 | the Source of the Modified Version. 139 | 140 | 141 | Aggregating or Linking the Package 142 | 143 | (7) You may aggregate the Package (either the Standard Version or 144 | Modified Version) with other packages and Distribute the resulting 145 | aggregation provided that you do not charge a licensing fee for the 146 | Package. Distributor Fees are permitted, and licensing fees for other 147 | components in the aggregation are permitted. The terms of this license 148 | apply to the use and Distribution of the Standard or Modified Versions 149 | as included in the aggregation. 150 | 151 | (8) You are permitted to link Modified and Standard Versions with 152 | other works, to embed the Package in a larger work of your own, or to 153 | build stand-alone binary or bytecode versions of applications that 154 | include the Package, and Distribute the result without restriction, 155 | provided the result does not expose a direct interface to the Package. 156 | 157 | 158 | Items That are Not Considered Part of a Modified Version 159 | 160 | (9) Works (including, but not limited to, modules and scripts) that 161 | merely extend or make use of the Package, do not, by themselves, cause 162 | the Package to be a Modified Version. In addition, such works are not 163 | considered parts of the Package itself, and are not subject to the 164 | terms of this license. 165 | 166 | 167 | General Provisions 168 | 169 | (10) Any use, modification, and distribution of the Standard or 170 | Modified Versions is governed by this Artistic License. By using, 171 | modifying or distributing the Package, you accept this license. Do not 172 | use, modify, or distribute the Package, if you do not accept this 173 | license. 174 | 175 | (11) If your Modified Version has been derived from a Modified 176 | Version made by someone other than you, you are nevertheless required 177 | to ensure that your Modified Version complies with the requirements of 178 | this license. 179 | 180 | (12) This license does not grant you the right to use any trademark, 181 | service mark, tradename, or logo of the Copyright Holder. 182 | 183 | (13) This license includes the non-exclusive, worldwide, 184 | free-of-charge patent license to make, have made, use, offer to sell, 185 | sell, import and otherwise transfer the Package with respect to any 186 | patent claims licensable by the Copyright Holder that are necessarily 187 | infringed by the Package. If you institute patent litigation 188 | (including a cross-claim or counterclaim) against any party alleging 189 | that the Package constitutes direct or contributory patent 190 | infringement, then this Artistic License to you shall terminate on the 191 | date that such litigation is filed. 192 | 193 | (14) Disclaimer of Warranty: 194 | THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS 195 | IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED 196 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR 197 | NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL 198 | LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL 199 | BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 200 | DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, EVEN IF 201 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 202 | -------------------------------------------------------------------------------- /nimrod.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """ 3 | Extract data from UK Met Office Rain Radar NIMROD image files. 4 | 5 | Parse NIMROD format image files, display header data and allow extraction of 6 | raster image to an ESRI ASCII (.asc) format file. A bounding box may be 7 | specified to clip the image to the area of interest. Can be imported as a 8 | Python module or run directly as a command line script. 9 | 10 | Author: Richard Thomas 11 | Version: 2.00 (27 November 2021) 12 | Public Repository: https://github.com/richard-thomas/MetOffice_NIMROD 13 | 14 | Command line usage: 15 | python nimrod.py [-h] [-q] [-x] [-bbox XMIN XMAX YMIN YMAX] [infile] [outfile] 16 | 17 | positional arguments: 18 | infile (Uncompressed) NIMROD input filename 19 | outfile Output raster filename (*.asc) 20 | 21 | optional arguments: 22 | -h, --help show this help message and exit 23 | -q, --query Display metadata 24 | -x, --extract Extract raster file in ASC format 25 | -bbox XMIN XMAX YMIN YMAX 26 | Bounding box to clip raster data to 27 | 28 | Note that any bounding box must be specified in the same units and projection 29 | as the input file. The bounding box does not need to be contained by the input 30 | raster but must intersect it. 31 | 32 | Example command line usage: 33 | python nimrod.py -bbox 279906 285444 283130 290440 34 | -xq 200802252000_nimrod_ng_radar_rainrate_composite_1km_merged_UK_zip 35 | plynlimon_catchments_rainfall.asc 36 | 37 | Example Python module usage: 38 | import nimrod 39 | a = nimrod.Nimrod(open( 40 | '200802252000_nimrod_ng_radar_rainrate_composite_1km_merged_UK_zip', 41 | 'rb')) 42 | a.query() 43 | a.extract_asc(open('full_raster.asc', 'w')) 44 | a.apply_bbox(279906, 285444, 283130, 290440) 45 | a.query() 46 | a.extract_asc(open('clipped_raster.asc', 'w')) 47 | 48 | Notes: 49 | 1. Valid for v1.7 and v2.6-4 of NIMROD file specification 50 | 2. Assumes image origin is top left (i.e. that header[24] = 0) 51 | 3. Tested on UK and European composite 1km and 5km data, using Python 3.9 52 | in Windows 10 53 | 54 | Copyright (c) 2021 Richard Thomas 55 | (Nimrod.__init__() method based on read_nimrod.py by Charles Kilburn Aug 2008) 56 | 57 | This program is free software: you can redistribute it and/or modify 58 | it under the terms of the Artistic License 2.0 as published by the 59 | Open Source Initiative (http://opensource.org/licenses/Artistic-2.0) 60 | 61 | This program is distributed in the hope that it will be useful, 62 | but WITHOUT ANY WARRANTY; without even the implied warranty of 63 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 64 | """ 65 | 66 | import argparse 67 | import array 68 | import struct 69 | import sys 70 | 71 | 72 | class Nimrod: 73 | """Reading, querying and processing of NIMROD format rainfall data files.""" 74 | 75 | class RecordLenError(Exception): 76 | """ 77 | Exception Type: NIMROD record length read from file not as expected. 78 | """ 79 | 80 | def __init__(self, actual, expected, location): 81 | self.message = ( 82 | "Incorrect record length %d bytes (expected %d) at %s." 83 | % (actual, expected, location) 84 | ) 85 | 86 | class HeaderReadError(Exception): 87 | """Exception Type: Read error whilst parsing NIMROD header elements.""" 88 | 89 | pass 90 | 91 | class PayloadReadError(Exception): 92 | """Exception Type: Read error whilst parsing NIMROD raster data.""" 93 | 94 | pass 95 | 96 | class BboxRangeError(Exception): 97 | """ 98 | Exception Type: Bounding box specified out of range of raster image. 99 | """ 100 | 101 | pass 102 | 103 | def __init__(self, infile): 104 | """ 105 | Parse all header and data info from a NIMROD data file into this object. 106 | (This method based on read_nimrod.py by Charles Kilburn Aug 2008) 107 | 108 | Args: 109 | infile: NIMROD file object opened for binary reading 110 | Raises: 111 | RecordLenError: NIMROD record length read from file not as expected 112 | HeaderReadError: Read error whilst parsing NIMROD header elements 113 | PayloadReadError: Read error whilst parsing NIMROD raster data 114 | """ 115 | 116 | def check_record_len(infile, expected, location): 117 | """ 118 | Check record length in C struct is as expected. 119 | 120 | Args: 121 | infile: file to read from 122 | expected: expected value of record length read 123 | location: description of position in file (for reporting) 124 | Raises: 125 | HeaderReadError: Read error whilst reading record length 126 | RecordLenError: Unexpected NIMROD record length read from file 127 | """ 128 | 129 | # Unpack length from C struct (Big Endian, 4-byte long) 130 | try: 131 | (record_length,) = struct.unpack(">l", infile.read(4)) 132 | except Exception: 133 | raise Nimrod.HeaderReadError 134 | if record_length != expected: 135 | raise Nimrod.RecordLenError(record_length, expected, location) 136 | 137 | # Header should always be a fixed length record 138 | check_record_len(infile, 512, "header start") 139 | 140 | try: 141 | # Read first 31 2-byte integers (header fields 1-31) 142 | gen_ints = array.array("h") 143 | gen_ints.fromfile(infile, 31) 144 | gen_ints.byteswap() 145 | 146 | # Read next 28 4-byte floats (header fields 32-59) 147 | gen_reals = array.array("f") 148 | gen_reals.fromfile(infile, 28) 149 | gen_reals.byteswap() 150 | 151 | # Read next 45 4-byte floats (header fields 60-104) 152 | spec_reals = array.array("f") 153 | spec_reals.fromfile(infile, 45) 154 | spec_reals.byteswap() 155 | 156 | # Read next 56 characters (header fields 105-107) 157 | characters = array.array("B") 158 | characters.fromfile(infile, 56) 159 | 160 | # Read next 51 2-byte integers (header fields 108-) 161 | spec_ints = array.array("h") 162 | spec_ints.fromfile(infile, 51) 163 | spec_ints.byteswap() 164 | 165 | except Exception: 166 | infile.close() 167 | raise Nimrod.HeaderReadError 168 | 169 | check_record_len(infile, 512, "header end") 170 | 171 | # Extract strings and make duplicate entries to give meaningful names 172 | chars = characters.tobytes().decode() 173 | self.units = chars[0:8] 174 | self.data_source = chars[8:32] 175 | self.title = chars[32:55] 176 | 177 | # Store header values in a list so they can be indexed by "element 178 | # number" shown in NIMROD specification (starts at 1) 179 | self.hdr_element = [None] # Dummy value at element 0 180 | self.hdr_element.extend(gen_ints) 181 | self.hdr_element.extend(gen_reals) 182 | self.hdr_element.extend(spec_reals) 183 | self.hdr_element.extend([self.units]) 184 | self.hdr_element.extend([self.data_source]) 185 | self.hdr_element.extend([self.title]) 186 | self.hdr_element.extend(spec_ints) 187 | 188 | # Duplicate some of values to give more meaningful names 189 | self.nrows = self.hdr_element[16] 190 | self.ncols = self.hdr_element[17] 191 | self.n_data_specific_reals = self.hdr_element[22] 192 | self.n_data_specific_ints = self.hdr_element[23] + 1 193 | # Note "+ 1" because header value is count from element 109 194 | self.y_top = self.hdr_element[34] 195 | self.y_pixel_size = self.hdr_element[35] 196 | self.x_left = self.hdr_element[36] 197 | self.x_pixel_size = self.hdr_element[37] 198 | 199 | # Calculate other image bounds (note these are pixel centres) 200 | self.x_right = self.x_left + self.x_pixel_size * (self.ncols - 1) 201 | self.y_bottom = self.y_top - self.y_pixel_size * (self.nrows - 1) 202 | 203 | # Read payload (actual raster data) 204 | array_size = self.ncols * self.nrows 205 | check_record_len(infile, array_size * 2, "data start") 206 | 207 | self.data = array.array("h") 208 | try: 209 | self.data.fromfile(infile, array_size) 210 | self.data.byteswap() 211 | except Exception: 212 | infile.close() 213 | raise Nimrod.PayloadReadError 214 | 215 | check_record_len(infile, array_size * 2, "data end") 216 | infile.close() 217 | 218 | def query(self): 219 | """Print complete NIMROD file header information.""" 220 | 221 | print("NIMROD file raw header fields listed by element number:") 222 | print("General (Integer) header entries:") 223 | for i in range(1, 32): 224 | print(" ", i, "\t", self.hdr_element[i]) 225 | print("General (Real) header entries:") 226 | for i in range(32, 60): 227 | print(" ", i, "\t", self.hdr_element[i]) 228 | print( 229 | "Data Specific (Real) header entries (%d):" 230 | % self.n_data_specific_reals 231 | ) 232 | for i in range(60, 60 + self.n_data_specific_reals): 233 | print(" ", i, "\t", self.hdr_element[i]) 234 | print( 235 | "Data Specific (Integer) header entries (%d):" 236 | % self.n_data_specific_ints 237 | ) 238 | for i in range(108, 108 + self.n_data_specific_ints): 239 | print(" ", i, "\t", self.hdr_element[i]) 240 | print("Character header entries:") 241 | print(" 105 Units: ", self.units) 242 | print(" 106 Data source: ", self.data_source) 243 | print(" 107 Title of field: ", self.title) 244 | 245 | # Print out info & header fields 246 | # Note that ranges are given to the edge of each pixel 247 | print( 248 | "\nValidity Time: %2.2d:%2.2d on %2.2d/%2.2d/%4.4d" 249 | % ( 250 | self.hdr_element[4], 251 | self.hdr_element[5], 252 | self.hdr_element[3], 253 | self.hdr_element[2], 254 | self.hdr_element[1], 255 | ) 256 | ) 257 | print( 258 | "Easting range: %.1f - %.1f (at pixel steps of %.1f)" 259 | % ( 260 | self.x_left - self.x_pixel_size / 2, 261 | self.x_right + self.x_pixel_size / 2, 262 | self.x_pixel_size, 263 | ) 264 | ) 265 | print( 266 | "Northing range: %.1f - %.1f (at pixel steps of %.1f)" 267 | % ( 268 | self.y_bottom - self.y_pixel_size / 2, 269 | self.y_top + self.y_pixel_size / 2, 270 | self.y_pixel_size, 271 | ) 272 | ) 273 | print("Image size: %d rows x %d cols" % (self.nrows, self.ncols)) 274 | 275 | def apply_bbox(self, xmin, xmax, ymin, ymax): 276 | """ 277 | Clip raster data to all pixels that intersect specified bounding box. 278 | 279 | Note that existing object data is modified and all header values 280 | affected are appropriately adjusted. Because pixels are specified by 281 | their centre points, a bounding box that comes within half a pixel 282 | width of the raster edge will intersect with the pixel. 283 | 284 | Args: 285 | xmin: Most negative easting or longitude of bounding box 286 | xmax: Most positive easting or longitude of bounding box 287 | ymin: Most negative northing or latitude of bounding box 288 | ymax: Most positive northing or latitude of bounding box 289 | Raises: 290 | BboxRangeError: Bounding box specified out of range of raster image 291 | """ 292 | 293 | # Check if there is no overlap of bounding box with raster 294 | if ( 295 | xmin > self.x_right + self.x_pixel_size / 2 296 | or xmax < self.x_left - self.x_pixel_size / 2 297 | or ymin > self.y_top + self.y_pixel_size / 2 298 | or ymax < self.y_bottom - self.x_pixel_size / 2 299 | ): 300 | raise Nimrod.BboxRangeError 301 | 302 | # Limit bounds to within raster image 303 | xmin = max(xmin, self.x_left) 304 | xmax = min(xmax, self.x_right) 305 | ymin = max(ymin, self.y_bottom) 306 | ymax = min(ymax, self.y_top) 307 | 308 | # Calculate min and max pixel index in each row and column to use 309 | # Note addition of 0.5 as x_left location is centre of pixel 310 | # ('int' truncates floats towards zero) 311 | xMinPixelId = int((xmin - self.x_left) / self.x_pixel_size + 0.5) 312 | xMaxPixelId = int((xmax - self.x_left) / self.x_pixel_size + 0.5) 313 | 314 | # For y (northings), note the first data row stored is most north 315 | yMinPixelId = int((self.y_top - ymax) / self.y_pixel_size + 0.5) 316 | yMaxPixelId = int((self.y_top - ymin) / self.y_pixel_size + 0.5) 317 | 318 | bbox_data = [] 319 | for i in range(yMinPixelId, yMaxPixelId + 1): 320 | bbox_data.extend( 321 | self.data[ 322 | i * self.ncols + xMinPixelId : 323 | i * self.ncols + xMaxPixelId + 1 324 | ] 325 | ) 326 | 327 | # Update object where necessary 328 | self.data = bbox_data 329 | self.x_right = self.x_left + xMaxPixelId * self.x_pixel_size 330 | self.x_left += xMinPixelId * self.x_pixel_size 331 | self.ncols = xMaxPixelId - xMinPixelId + 1 332 | self.y_bottom = self.y_top - yMaxPixelId * self.y_pixel_size 333 | self.y_top -= yMinPixelId * self.y_pixel_size 334 | self.nrows = yMaxPixelId - yMinPixelId + 1 335 | self.hdr_element[16] = self.nrows 336 | self.hdr_element[17] = self.ncols 337 | self.hdr_element[34] = self.y_top 338 | self.hdr_element[36] = self.x_left 339 | 340 | def extract_asc(self, outfile): 341 | """ 342 | Write raster data to an ESRI ASCII (.asc) format file. 343 | 344 | Args: 345 | outfile: file object opened for writing text 346 | """ 347 | 348 | # As ESRI ASCII format only supports square pixels, warn if not so 349 | if self.x_pixel_size != self.y_pixel_size: 350 | print( 351 | "Warning: x_pixel_size(%d) != y_pixel_size(%d)" 352 | % (self.x_pixel_size, self.y_pixel_size) 353 | ) 354 | 355 | # Write header to output file. Note that data is valid at the centre 356 | # of each pixel so "xllcenter" rather than "xllcorner" must be used 357 | outfile.write("ncols %d\n" % self.ncols) 358 | outfile.write("nrows %d\n" % self.nrows) 359 | outfile.write("xllcenter %d\n" % self.x_left) 360 | outfile.write("yllcenter %d\n" % self.y_bottom) 361 | outfile.write("cellsize %.1f\n" % self.y_pixel_size) 362 | outfile.write("nodata_value %.1f\n" % self.hdr_element[38]) 363 | 364 | # Write raster data to output file 365 | for i in range(self.nrows): 366 | for j in range(self.ncols - 1): 367 | outfile.write("%d " % self.data[i * self.ncols + j]) 368 | outfile.write("%d\n" % self.data[i * self.ncols + self.ncols - 1]) 369 | outfile.close() 370 | 371 | 372 | # ------------------------------------------------------------------------------ 373 | # Handle if called as a command line script 374 | # (And as an example of how to invoke class methods from an importing module) 375 | # ------------------------------------------------------------------------------ 376 | 377 | if __name__ == "__main__": 378 | parser = argparse.ArgumentParser( 379 | description="Extract information and data from a NIMROD format file", 380 | epilog="""Note that any bounding box must be specified in the same 381 | units and projection as the input file. The bounding box 382 | does not need to be contained by the input raster but 383 | must intersect it.""", 384 | ) 385 | parser.add_argument( 386 | "-q", "--query", action="store_true", help="Display metadata" 387 | ) 388 | parser.add_argument( 389 | "-x", 390 | "--extract", 391 | action="store_true", 392 | help="Extract raster file in ASC format", 393 | ) 394 | parser.add_argument( 395 | "infile", 396 | nargs="?", 397 | type=argparse.FileType("rb"), 398 | default=sys.stdin, 399 | help="(Uncompressed) NIMROD input filename", 400 | ) 401 | parser.add_argument( 402 | "outfile", 403 | nargs="?", 404 | type=argparse.FileType("w"), 405 | default=sys.stdout, 406 | help="Output raster filename (*.asc)", 407 | ) 408 | parser.add_argument( 409 | "-bbox", 410 | type=float, 411 | nargs=4, 412 | metavar=("XMIN", "XMAX", "YMIN", "YMAX"), 413 | help="Bounding box to clip raster data to", 414 | ) 415 | args = parser.parse_args() 416 | 417 | if not args.query and not args.extract: 418 | parser.print_help() 419 | sys.exit(1) 420 | 421 | # Initialise data object by reading NIMROD file 422 | # (Only trap record length exception as others self-explanatory) 423 | try: 424 | rainfall_data = Nimrod(args.infile) 425 | except Nimrod.RecordLenError as error: 426 | sys.stderr.write("ERROR: %s\n" % error.message) 427 | sys.exit(1) 428 | 429 | if args.bbox: 430 | sys.stderr.write("Trimming NIMROD raster to bounding box...\n") 431 | try: 432 | rainfall_data.apply_bbox(*args.bbox) 433 | except Nimrod.BboxRangeError: 434 | sys.stderr.write("ERROR: bounding box not within raster image.\n") 435 | sys.exit(1) 436 | 437 | # Perform query after any bounding box trimming to allow sanity checking of 438 | # size of resulting image 439 | if args.query: 440 | rainfall_data.query() 441 | 442 | if args.extract: 443 | sys.stderr.write("Extracting NIMROD raster to ASC file...\n") 444 | sys.stderr.write( 445 | " Outputting data array (%d rows x %d cols = %d pixels)\n" 446 | % ( 447 | rainfall_data.nrows, 448 | rainfall_data.ncols, 449 | rainfall_data.nrows * rainfall_data.ncols, 450 | ) 451 | ) 452 | rainfall_data.extract_asc(args.outfile) 453 | --------------------------------------------------------------------------------