fly 0.24.1 (2026-10-17)
-
The README says what fly does in plain words, and shows real airphotos (#104). No code change.
- The opening says what a historic photo gives a site: a reference for what to restore toward, and a baseline for measuring change. It also says that an elevation model makes film footprints more accurate and corrects the flying-height errors fly can identify.
-
The figure shows the upper Bulkley River near McKilligan Road in 1971 and 1981, from two photos fly georeferenced.
data-raw/readme_figure.Rmakes it. -
Shared sections. An ecosystem table shared with the sibling packages replaces the function list. DESCRIPTION’s
Descriptionfollows the opening, and it now says that fly downloads thumbnails, not scans.
-
The changelog has one section per release. Every release sat under a leftover “development version” heading, so the website showed all of them as a single section. Each is now
# fly x.y.z (date). - The function reference is grouped by purpose: estimate footprints, choose photos for an area, download, and georeference.
fly 0.24.0 (2026-10-08)
-
fly_footprint(dem =)now names the frames under the terrain that sit on a roll-height whose catalogue centroids fly#97 labelledmisplaced(#99). Geometry and columns are unchanged: the footprint is still drawn at the catalogue’s centroid, at nominal scale.-
What it says. Beside the “terrain at or above the aircraft” warning, a second one lists
bc770262042 m,bc770701158 m,bc770721829 m and 1981 m, orbc770871158 m, with a count per roll-height, wherever your DEM puts one of their frames under the aircraft. On each of these roll-heights the logbook pages write the catalogued height above sea level for at least 90% of the frames they read, so the generic advice to check the height’s datum may point the wrong way. -
What it does not. It is not a census. The list is what fly#97 labelled
misplaced, 5 of the 9 roll-heights it measured, so an unlisted roll-height is not thereby clean: most were never measured, and four were measured and labelled otherwise. On these five roll-heights it names only the frames under the terrain. Of the 311 frames on them that fly#93’s census holds (in band above sea level but below it over the ground, and under the terrain), 112 are under MRDEM’s terrain and the other 199 are refused by the height check with its own warning; frames in band are sized from the DEM as usual. Nothing is named withoutdem.bc7718andbc80117, left unsettled by fly#97, are not named. -
One fact, one file. The warning reads
flying_height_image_overlap_keys.csv(location == "misplaced") directly rather than a copied ledger, so re-runningdata-raw/height_measure-image_overlap.Rchanges what it names. A test stops a re-run that labels a keymisplacedwithout the step overstated, for which the warning’s wording would be false.
-
What it says. Beside the “terrain at or above the aircraft” warning, a second one lists
fly 0.23.3 (2026-10-08)
-
bc77087’s disputed logbook digit is settled as 3, so its page writes the catalogue’s 3,800 ft (#101). No code change, and every roll table is byte-identical.- The blind reads. A third blind read, under a rule fixed before it, could not choose between 3 and 5, leaning 3, and did not list 7. With fly#93’s 3 and fly#97’s “3 or 7”, the three blind reads disagreed.
- The human read. A human read of the page then settled the digit as 3. It was not blind, and the transcription note records it as such.
-
What it means. Over the 689 frames fly#95’s test reads, no transcribed logbook page puts the ground under the catalogued height. Among fly#97’s nine keys,
bc77087was the one page whose reading could have. The transcription keeps 3,800 ft. Seeinst/notes/terrain-correction.md, “What the frames under the terrain covered”.
fly 0.23.2 (2026-10-07)
-
On five of the nine roll-heights where spacing rejected both readings of the frames under the terrain, the photos show the catalogue’s centroid step is longer than the air base, so the rejection says nothing about scale (#97). No code change, and every roll table is byte-identical.
- The instrument. It measures the overlap the photos themselves show: the image shift between adjacent thumbnails, which reads no scale, height or centroid. Synthetic, unrelated-pair, crew-written-overlap and known-error controls ran before the nine were measured.
-
What it found.
- On five roll-heights, nominal scale stands as the default, unrefuted rather than confirmed.
- By their logbooks against MRDEM, the frames under the terrain on four of them that a logbook row reaches are not over the ground photographed.
bc7718andbc80117were flown at ~85% overlap, where the two readings cannot be told apart. - The figures and their qualifications are in
inst/notes/terrain-correction.md, “What the frames under the terrain covered”.
-
An open question. One page’s disputed digit (
bc77087, 3,800 or 7,800 ft) decides whether a logbook page puts the ground under a catalogued height, the result fly#95 was looking for. -
New data.
-
flying_height_image_overlap_*.csvship every pair, control and verdict. The suite recomputes the verdicts, the note’s table, and the figures it lists. - Five more rolls’ logbooks were transcribed blind. That moves fly#95’s logbook counts but none of its verdicts.
- A per-strip transcription of headings and places is committed as
data-raw/flying_height_logbook_strips.csv.
-
-
Not done. Whether
fly_footprint()should tell the caller about a known misplaced centroid (#99).
fly 0.23.1 (2026-10-06)
-
Whether a frame’s catalogued height is a height above ground was tested, and these instruments cannot settle it (#95). No code change, and every roll table is byte-identical.
- The question. fly#93 left 374 frames under ground at or above the catalogued aircraft and 176 roll-heights where spacing fits nominal scale, both consistent with a height recorded above ground. Together they are 188 roll-heights, judged on the 2,956 frames the two census files hold on them.
-
What was measured. A rule was fixed before any per-roll-height number: spacing first, then the logbook. Spacing supports reading the height as above ground on 1 roll-height, cannot tell it from nominal on 126 and rejects it on 61. The one it supports,
bc56021219 m, has a logbook writing the catalogue’s own 4,000 ft under an M.S.L. header, so nothing tables. Across the 576 logbook-read frames, the page writes the catalogue’s height on 558, and no page puts the ground under it. -
Why “cannot settle”, not “false”. On a key, the overlap read as above ground is a fixed function of nominal’s overlap and
flying_height / (scale x focal). Every frame here is in band above sea level, so the two readings are under 1.6 times apart, where spacing stops separating them. The undecided readings differ by a median 12% in width and up to 49%. Where nominal fits, the refutations sit a median 0.028 outside the window. Nominal scale stays where it was. -
New data.
flying_height_terrain_nonpositive.csvships the 374 frames under the aircraft, from the census that found them.flying_height_above_ground.csvhas one row per roll-height. The suite recomputes the spacing verdict and the note’s tables from them. - Not done. On nine roll-heights (157 of those 374 frames) spacing rejects nominal scale as well, so their fallback is not supported either (#97).
fly 0.23.0 (2026-10-06)
-
BW and colour frames out of the height band only through terrain are now settled per roll-height (#93).
-
What changed. 60 BW/colour roll-heights (1,344 frames) join fly#91’s
terraintail inflying_height_rolls.csvat factor 1 (scale_wrong). Their catalogued height is in band above sea level and the ground takes them out of it. With adem, they were drawn at nominal scale, about twice the width the spacing supports, and are now sized from the logbook height (height_source = "corrected_roll_table"). Without ademnothing changes. No tabled row and no verdict of an earlier tail moved. Two excluded rows of earlier tails (fly#60’sbc78104at 1,295 m and fly#72’sbc79029) said “no logbook page covers these frames” on rolls whose transcribed pages simply end before those frames, and now say so. -
How it was established.
data-raw/height_measure-terrain_tail.Rcensuses the stratum: 4,773 frames on 298 roll-heights, against an estimate of about 7,000 from 12 random draws. #72’s rule runs unchanged. A pre-registered amendment evaluates its spacing condition first, and excludes the 208 roll-heights where that condition cannot hold whatever a logbook says. 182 pages on 60 rolls were transcribed blind, and six control pages agree on height and lens. The census ships asflying_height_terrain_frames.csvand_population.csv, and the suite recomputes the census, the amendment and the note’s tables from them. - Not done. 374 frames sit under ground at or above the catalogued aircraft, and 176 roll-heights fit nominal scale, which is consistent with a height recorded above ground (#95).
-
What changed. 60 BW/colour roll-heights (1,344 frames) join fly#91’s
fly 0.22.0 (2026-10-06)
-
Three infrared roll-heights with a wrong
scaleare now sized from the height flown (#91).-
What changed. v0.21.0 brought infrared film into the #54 height check, and 56 IR frames fell outside the band. With a
dem, they were drawn at nominal scale:- about twice the width the spacing supports on
bc5312andbci12; - about half of it on
bci9.
flying_height_rolls.csvat factor 1 (scale_wrong).fly_footprint(dem = )therefore sizes the 56 frames from the logbook height and reportsheight_source = "corrected_roll_table". Without ademnothing changes; every film frame is nominal there. - about twice the width the spacing supports on
- How it was established. The rule was committed before the logbook pages were read, and the pages were then transcribed blind. They give 11.0, 12.08 and 19.5 thousand feet, the catalogued heights. Spacing at those heights is 0.594, 0.620 and 0.620, inside the 0.557-0.780 window, and at nominal scale it is 0.805, 0.807 and 0.228.
-
A new
tail,terrain.bci9is settled alongside fly#72’snear_uppersample.bc5312andbci12are in band above sea level and leave it only through the ground beneath them, which no existing tail held, so they carrytail = "terrain"under the same rule. -
One amendment, written before the read. The logbook scale pattern accepted only
1:N, and these pages write1/15,840. It now accepts both, and no BW/colour row changes. Every BW/colour row of both roll tables is byte-identical to v0.21.0. - Not done. The same terrain-only population exists among BW/colour frames: 12 of 2,500 random draws, roughly 7,000 frames. No rule reaches it yet (#93).
-
What changed. v0.21.0 brought infrared film into the #54 height check, and 56 IR frames fell outside the band. With a
fly 0.21.0 (2026-10-02)
-
Infrared film is now sized as the 9-inch negative, because that was measured (#89).
-
What changed.
Film - BW IR(771 frames, 7 rolls) andFilm - Colour IR(3,054 frames, 25 rolls) wereunknown_formatwith empty geometry. They are now sized fromnegative_sizewithfootprint_basisset to their media value.fly_coverage(),fly_select(),fly_overlap()andfly_filter()now count them, and the #54flying_heightcheck applies: 3,769 sit inside the band and 56 fall back to nominal scale. Those 56 are on three roll-heights where spacing fits the reported height and rejects nominal: a right height beside a wrongscale. They are drawn at the wrong width, with or without a DEM, until they are tabled (#91). -
Callers already sizing IR through
format_size. These frames used to skip the #54 check; it now applies to them too. -
How it was established.
data-raw/format_measure-infrared_film.Rused three witnesses:- Adjacent-frame spacing, fly#60’s instrument, against the BW/colour window 0.557–0.780. 27 of 32 rolls fit 9 inches and exclude 5 inches and 70 mm.
- Thumbnails: the 4 rolls that have any match BW/colour film in aspect and collar. That is weak evidence, since a 5-inch or 70 mm frame is square too.
- Logbooks, 27 pages read by hand. Every page that writes a format writes 9 x 9 or names an RC 10, and the rest name camera bodies that also flew BW/colour rolls already sized at 9 inches.
-
Where the evidence is thin.
- The pre-registered rule stopped on four rolls whose spacing no format fits (9-inch overlap 0.19–0.27 on the nominal reading, where a smaller format fits worse still). An amendment written after those numbers, and recorded as such, named them
fits_no_format. -
bcf517(13 frames) is ambiguous: one of its two readings favours 5 inches. - Spacing cannot separate 23 cm from 18 cm. Six rolls’ logbooks write 9 x 9 or name an RC 10, and 11 more name camera bodies that also flew 9-inch BW/colour, but 15 rolls have no logbook page at all.
- The pre-registered rule stopped on four rolls whose spacing no format fits (9-inch overlap 0.19–0.27 on the nominal reading, where a smaller format fits worse still). An amendment written after those numbers, and recorded as such, named them
-
Reproducibility. Five
inst/extdata/infrared_film_*.csvship, and the suite recomputes every verdict from them. Seeinst/notes/camera-formats.md, “Infrared film is the 9-inch negative”. -
Film rotations. The 26 infrared rolls fly#53 measured were recorded
legs_unscorableonly because their footprints were empty. Re-measured:bcc23(1969, 305 mm) ships at 270, sofilm_rotations.csvnow has 57 rolls.bcf07060has one decisive leg, and the other 24 have no thumbnails at all. Every other roll’s row is unchanged.data-raw/georef_calibrate-film_rotations.Rnow dates each roll by when it was measured, so a partial re-run no longer restamps the rest. - Earlier measurements are unchanged. The #54, #58, #65 and #80 scripts now spell out the BW/colour set they were measured over, so re-running them reproduces what shipped.
-
What changed.
fly 0.20.0 (2026-10-02)
-
fly_georef()now georeferences film from a shipped table of measured per-roll rotations, and every other film roll is recorded with the reason it is not in it (#53). A rotated film frame used to be refused unless you supplied the roll’srotation. 56 rolls now resolve frominst/extdata/film_rotations.csv: 44 at 90, 10 at 270 and 2 at 0. Your ownrotationcolumn still wins. The other 6,660 film rolls in the catalogue snapshot are infilm_rotations_excluded.csvwith their state, and the refusal warning names it.-
New
fly_rotation_calibrate()measures any roll the table does not cover, by the same method and rule that built it. Pass the roll’s catalogue rows and join the result ontorotation. - The method. Adjacent-frame overlap correlation at all four rotations. A leg is decisive only when a sign test beats every rival rotation. A roll ships on two or more decisive legs that agree, two of them at least 90 degrees apart, because a roll scanned in a fixed geographic orientation would agree with itself on two legs closer than that. The rule was fixed before any thumbnail was read, and its seven amendments are dated and reasoned.
- The sample. 116 rolls were examined across 27 roll-series × 5-year strata. No measured roll’s legs disagreed, and no roll’s missions disagreed with each other.
- Why the key is the roll. The shipped rolls fall into eras: 270 for the 305 mm rolls of 1964-73, 0 for the two 153 mm rolls of 1967-68, and 90 for every roll from 1974, except bcc00116 (2000), which is 270. Three measured rolls that did not ship also each have a decisive leg against their era: bc7397 (1972) at 90, bc5650 (1975) at 0, and bcb00037 (2000) at 270. A per-era default would have written bcc00116 a half turn wrong.
- Two corrections to fly#26. One of its four film legs (bc83062 108-118) is not a leg in today’s catalogue, and another (152-162) flies 251 degrees, not 62. Its rejection of a fixed geographic orientation rested on those two, so it is re-established on reverse legs instead. Its “cardinal legs are degenerate” premise was measured and dropped.
-
What the table does not cover. Every verdict is for the public thumbnails, not full-resolution scans, and overlap cannot detect a scan mirrored about the flight line. Infrared film is not sized by
fly_footprint()at all (#89), so its rolls are unmeasured.
-
New
-
The last frame of a flight line is no longer rotated onto the turn (#87). Frame numbers run straight through a turn, so
fly_bearing()gave each line’s last frame the azimuth of the jump to the next line. A frame now takes the backward bearing when its forward step is zero length, or more than 1.5x a backward step that itself continues the line. Over the 1,670,471 catalogue frames this changes the bearing of 48,139 (2.88%), 5,390 of them digital. 42,001 move by more than 10 degrees, 4,225 of them digital, and those digital frames had been georeferenced onto the turn. A bearing that could only come from a zero-length step isNArather than 0 (10 frames). Footprints, coverage and georeferencing change only for the frames whose bearing changed.
fly 0.19.3 (2026-10-01)
-
Photo parallax, as built, cannot say what surface the camera saw at the photo date. No code changes (#82).
- The question. fly#80 left one question to VRI stand origin: was the canopy MRDEM’s DSM carries there when the photo was taken?
-
The instrument.
data-raw/dem_measure-photo_parallax.Rreads it from the parallax between frames adjacent by number. The question needs a ratio: canopy seen on mid-aged stands over canopy seen on old stands. - What failed. A synthetic built so that fly#80’s VRI model is exactly true, pooled over the seven pilot frames the gates admitted, missed the known ratio in both MRDEM sources: radar 1.356 against 0.860, lidar 0.793 against 0.550.
- So no canopy slope was computed on the real draw. Why it fails is not established (#85).
- fly#80’s estimate stands alone. A DSM is worse on 15.1% of the 1970s frames where canopy matters.
-
What ships. The synthetic controls,
inst/extdata/dem_parallax_synthetic.csv, from which the suite rebuilds the tables ininst/notes/terrain-correction.md.
fly 0.19.2 (2026-10-01)
-
A forested frame is sized from bare earth, and measured, that does not matter:
fly_footprint()is unchanged (#80). Over forest the camera images the canopy, so a frame sized from MRDEM’s bare-earth DTM is drawn slightly too wide. Sized throughfly_footprint()on both MRDEM’s DTM and its DSM (published on the same grid), over a probability sample of 612 film frames weighted back to the 1,437,124 DEM-eligible ones with a canopy value, the DSM shrinks a frame by a weighted median of 0.17% of width and 0.46% at the 95th percentile. 1.2% of frames move more than 1%, 99.5% of them by weight fine-scale (2.1% of frames at 1:15000 or finer). The rule fixed before the run called it material at a 95th percentile of 1%, so nothing changed, and the DTM stays the recommendation. A ray-cast of the true footprint confirms the first-order shift to a weighted median 1.68e-4, and a DSM does not degrade the rectangle model (95th-percentile relief residual 2.89% against 2.86%). -
MRDEM’s DSM is the imaged surface to within about a tenth, checked against LidarBC. Over BC land 88.3% of MRDEM comes from radar, where NRCan’s DTM is the DSM minus a forest-removal model, and NRCan’s own lidar mosaic covers almost none of those cells (0 of 3,000 random points). Over 150 public LidarBC tiles (2019–2025) under random radar cells, MRDEM overstates the canopy (7.75 m against 4.25 m, median) but its DTM sits 2.50 m below the lidar ground, so the lidar surface stands above MRDEM’s DTM by 0.916 of MRDEM’s
DSM − DTM. Over sea the DSM is within 0.03 m of the DTM. -
fly#65’s “a canopy can reverse the area verdict” does not hold at the canopy BC has. Its first-order table assumed a uniform canopy on land and tied near 30 m; with each frame’s own
DSM − DTMin its place (a median 13.31 m per land cell coastal), still to first order, the land-and-sea mean stays closer than the land-only mean (median −0.00127, against −0.00287 on bare earth). A canopy also feeds theflying_heightcheck, so passing a DSM can move a frame near the band’s edge between"implausible"and"reported": 2 of the 612 did.data-raw/dem_measure-canopy_height.Rprints every figure and shipsinst/extdata/dem_canopy_*.csv, from which the suite rebuilds the tables ininst/notes/terrain-correction.md.
fly 0.19.1 (2026-09-30)
-
A coastal frame’s sea surface is an elevation, not an error, and
fly_footprint()is unchanged (#65). v0.14.0’s notes said MRDEM’s near-zero surface over near-shore sea drags a coastal frame’s mean “toward sea level”. MRDEM carries that sea at about 0.14 m, not as nodata, and over water it is the surface the photo images. Against a ray-cast of the true footprint on MRDEM’s bare earth, over 1,242 coastal frames, averaging land and sea (what the package does) is closer on area than the land-only mean the issue proposed: 0.34% against 0.67% of width, median. The land-only mean places the land edge better, misplacing 3.67% of the frame’s land against 4.43%. The rule fixed before the run asked whether the package’s 95th-percentile error on coastal frames exceeds inland. Pooled, it does not: 2.16% against 3.18% on area, and 15.05% against 15.87% on the land edge. So no remedy was triggered. Coastal frames are 95,222 of 1,437,147 DEM-eligible film frames (6.63%). - Two qualifications, recorded rather than acted on. First, the land-edge pass is an artefact of relief: the coastal sample is flatter, and matched for relief the inland land-edge 95th is 15.06% against 15.05% coastal. Within four of five relief bins coastal is worse by 2 to 9 points, so the sea does make the package’s land edge worse. It does not make its area worse, even matched. Second, a canopy can reverse the area verdict. To first order, a uniform 30 m canopy on the land ties the two means and 60 m favours the land-only one. That is a property of sizing from a bare-earth DEM anywhere, filed as #80.
-
LidarBC was mostly nodata over sea in the two tiles probed, so the same coastal frame is sized nearer the land-only mean there. Where that leaves it under 95% covered, it warns about nodata inside the DEM’s extent.
data-raw/dem_measure-coastal_water.Rprints every figure above, and shipsinst/extdata/dem_coastal_frames.csv,dem_coastal_population.csvanddem_coastal_sites.csv, which the test suite rebuilds the note’s tables from.inst/notes/terrain-correction.mdexplains why the instrument first planned, adjacent-frame spacing, was dropped before any frame was measured.
fly 0.19.0 (2026-09-28)
-
Breaking: grayscale georeferenced output is now 2 bands, Gray + Alpha, where it was 1 band with
NoData = 0(#56).fly_georef()warps every frame with-dstalpha, so RGB stays 4 bands and, from a grayscale or RGB source, no output carries a NoData value. A consumer that reads band 1 alone, or treats 0 as fill, must now read the last band: 0 is fill, and on these outputs the opaque value is the datatype’s maximum (255 on Byte, 65535 on UInt16). Under-dstnodata 0, GDAL rewrote genuine black inside the frame as 1 so it would not read as fill (or deleted it wheresrcnodata = "0"was also given). Measured on the output of the 182 grayscale frames the mask was calibrated on, it hit 161 frames warped axis-aligned (every frame with no flight bearing): typically a few dozen pixels, up to 3.6% of a frame on roll bcb94081 (1250 x 1250 thumbnails on square footprints; an anisotropic scan resamples even when axis-aligned). On a footprint rotated 30 degrees it hit 41 frames, at most 0.26%, because resampling mixes a lone 0 with its neighbours. The Windows CI runner also gave a masked grayscale frame 2 bands where ubuntu and macOS gave 1 (#68). Band count is now one number, measured on macOS with sf’s GDAL 3.8.5 and asserted by the test suite on all three CI platforms. This supersedes 0.11.0’s “nothing downstream changes band count”.stac_airphoto_bcneeds a COG-writer change and a republish of grayscale frames (stac_airphoto_bc#36). -
fly_georef(mask = "border", srcnodata = ...)is now accepted, andsrcnodatais the fallback for frames whose mask declined (#69, absorbed into #56). The warp options were always either/or, so the 0.11.0 refusal guarded a combination GDAL never received and only made the fallback unreachable.srcnodatareaches a frame only whenfly_mask()declined it, and never alongside a mask that ran. A frame whose mask declines with nosrcnodatais now warned about, since its collar reaches the output as image data. The mask declined none of 10,105 public thumbnails, so on thumbnails this changes nothing; it matters for non-8-bit scans.data-raw/mask_measure-interior_zeros.Rreproduces both figures.
fly 0.18.0 (2026-09-28)
-
Half the r ≈ 2 mass was a wrong scale, not a mislabelled lens, and it is now drawn at its true width (#72). v0.12.0 said the film frames around twice their nominal scale were a 305 mm lens catalogued as 153, so falling back to nominal scale was right. The flight logbooks and the spacing between adjacent frames, read per roll-height over all 252 sampled frames beyond the band, split it: 24 roll-heights (120 sampled frames; their keys reach at most 3,227 catalogue frames, fewer once terrain brings some inside the band, where they were already sized from their height) carry the height the crew flew beside a
scalerecorded too small — mostly 1972–76 rolls at half the true denominator — and ship inflying_height_rolls.csvat factor 1,scale_wrong, under a newtailvalue,"near_upper". The fallback drew them at 1/r of their width, half at r = 2; they are now"corrected_roll_table"and sized from their height. On 21 roll-heights (82 frames) the logbook writes a 12” lens, which confirms the lens reading there, and those stay on nominal scale, as do the rest; each is listed inflying_height_rolls_excluded.csvwith its reason. -
The logbook height alone cannot tell the two defects apart — a wrong lens and a wrong scale both mean the crew flew the catalogued height — so for this tail the rule, fixed before the run, also requires the spacing to reject nominal scale, not merely fit the reported height. The near_upper set is a 600-frame sample, not a census;
inst/notes/terrain-correction.mdstates what it does not reach. -
data-raw/flying_height_logbooks.csvgains 185 rows from 142 newly read page files covering 54 roll labels. The readers worked blind; two things were added at merge and say so in theirnote: the 18 heights on 1984–86 sheets that write whole feet rather than thousands, and two rows marking the interior of a strip whose height changes along it as covered but unread. On these pagesfocal_mmis filled only where a focal length is written, never inferred from the camera model.
fly 0.17.0 (2026-09-27)
-
A frame beside a disputed roll-height on the same roll is now a third witness, and it settles five roll-heights no logbook does (#74). The rule was fixed before it was run. A neighbour one frame away, on the same roll, lens and scale, must itself sit in the band, and the catalogue’s height must stand in an exact named relation to it: ×10, ÷10, ÷100, ×10.764 or one leading digit. Exact means within what storing whole metres of a 0.3048 m/ft conversion can move a figure, and the catalogue both rounds and truncates. A figure converted at 3.28 ft/m can only be refused by it, never wrongly accepted. Every neighbour naming a relation must name the same one, and spacing must agree. A logbook vetoes it only by naming a different factor. The rule was run over all 521 roll-heights the logbook leaves excluded, in both tails.
bc5596204–211 (26,212 m, ten times frame 203) andbcb98013frame 52 (97,924 m, a leading 9 on its neighbours’ 7,924) leave #54’s ÷10.764, which drew them 10.3–11.2% narrow and 17.1% wide in linear size. Three lower-tail roll-heights beyond the issue come out of"implausible":bc7675609 → 6,096 m,bc87070396 → 3,962 m andbcc822701 → 7,010 m. The logbook corroborates each in rows its own rule could not accept -
The tolerance is the catalogue’s storage, not the logbook’s 2%, and that is load-bearing.
bc5596204–211 sit between 2,621 m and 2,438 m. The first is ×10 to 2 m; the second is ×10.764 to 30 m, only 0.12%. At 2% the two neighbours name different relations, and the generator’s control stops the run. The first form assumed rounding only. Code review found truncated heights (2,000 ft stored as 609 m), and that form had refusedbc7675; the corrected form changes that outcome and no other -
flying_height_rolls.csvgainswitness("logbook"|"sibling") andsibling_frame. A sibling row ships the neighbour’s catalogued height and has nologbook_ft. A leading-digit row’sfactoris the ratio it implies, and its cause isheight_leading_digit_added.flying_height_rolls_excluded.csvgainssibling_reason. The table now reaches 1,438 frames (1,130 lower, 308 upper), and 1,281 slipped frames stay on ÷10.764. No change tofly_footprint()’s logic
fly 0.16.0 (2026-09-26)
-
Where a logbook reads #54’s slipped frames, their height is ÷10, not ÷10.764, and those roll-heights are now carried in the roll table (#71). #54 divides
flying_heightby 3.28084² on 1,589 film frames, and ÷10 lands every one of them in the band just as well, so the terrain could not choose. The logbook pages covering the slipped frames of five rolls were fetched and read blind, with the catalogue values withheld from the reader. On four (bc78078,bc79027,bc79103,bcc00085), and on the fly#60 controlbc78065, the crew’s height is the catalogue’s ÷10: within 0.01%, and 0.9% off onbc79027. It sits 6.7–7.6% above ÷10.764 on every one. On the tabled roll-heights #54 was drawing frames 7.6–9.9% narrow (15–19% in area); width follows height above ground, so the error exceeds the 7.6% gap between the factors -
Six roll-heights (299 frames, 1978–2000) ship in
flying_height_rolls.csvat factor 0.1, causeheight_decimal_dropped, and becomeheight_source == "corrected_roll_table". The roll-table gate infly_footprint()now readsfactor != 1where it read> 1. The old form silently handed a factor below 1 back to #54’s repair, and a test goes red under it. Both roll tables gain atailcolumn ("lower"for fly#60,"upper"for #54’s slipped frames) -
fly_height_slip_factor()is unchanged, on purpose. The 1,290 slipped frames the table does not reach stay on ÷10.764, each listed inflying_height_rolls_excluded.csvwith a reason saying so. The five 2003 rolls have no logbook link, carry measured per-frame heights, and fit ÷10.764 better.bcc05001has no logbook either and is undecided: its heights land on round 500-ft altitudes under ÷10, while its ratio to scale favours ÷10.764.bc79027reads ÷10, but spacing rejects it, because its own page logs 80% forward overlap.bcb98013frame 52 is a leading-digit typo that no factor fits. That frame andbc5596, which has no page, are #74. The lower-tail rows of both tables are unchanged.data-raw/height_calibrate-lower_tail_rolls.Rnow settles both tails through one function and reproduces every figure
fly 0.15.0 (2026-09-26)
-
Half of the lower tail of
flying_heightis now settled per roll, and 389 of those 1,001 frames were never a height error (#60). #54 left 1,962 film frames reading under half the height their scale implies as"implausible", because ×10.764, ×10 and ×2 each explained a comparable share when pooled. Per roll they do not: the tail is 42 rolls, nearly each carrying one height, so it is 77 roll-heights rather than 1,962 frames. Two instruments that read none of the three disputed fields settle 22 of them (1,001 frames): the province’s scanned flight logbooks atflight_log_url, whose TRUE HEIGHT column the catalogue transcribed, and the spacing between frames adjacent by number, which must imply the ~60% forward overlap a flight is designed to - New
height_sourcevalue"corrected_roll_table", read from the newinst/extdata/flying_height_rolls.csvand keyed onfilm_roll,flying_height,focal_lengthandscaletogether, which reaches exactly the 1,001 measured frames. Ten roll-heights (511 frames) dropped a digit, a logbook “20.0” in thousands of feet catalogued as 2,000 ft; one (bc7280, 101 frames) dropped two, 60 m catalogued where 20,000 ft was flown. Eleven (389 frames) carry the right height beside a wrongscale, which is a remedy the issue did not list: for those the fallback #54 chose was the defect, drawing them two to ten times too wide. The height used is always the logbook’s, converted, never the catalogue’s times the factor, since the catalogue’s rounding survives the multiplication and leftbc728096 m short. Needs afilm_rollcolumn and adem; without a DEM a wrong-scale frame is still sized from its scale -
Every lower-tail roll-height the table does not correct is listed with its reason in
flying_height_rolls_excluded.csv(55 roll-heights, 961 frames — 30 with no logbook page, 8 where spacing rejects the logbook’s height, 5 where the covering row was not read, 4 no named multiple, 4 where the logbook names a different lens and nominal scale is already right, 2 where covering rows disagree, 2 under half covered), so an unlisted roll reads as unmeasured rather than clean. The test suite recomputes every row from the shipped sweep and asserts the two files partition the census exactly; every one of the 77 labels was re-derived from the per-frame logbook states by an independent predicate, and all agree -
Both instruments were held to known answers first. Spacing reads 0.63 on random in-band frames and 0.62 on #54’s slipped frames after repair, against 0.97 before. The logbooks were transcribed with three control rolls’ catalogue values withheld, and the two clean ones read back to the metre. The rule — logbook covers half the frames and 90% agree on a factor of 1, 10 or 100; no legible logbook lens contradicts the catalogue; spacing inside the random frames’ own 2.5–97.5% — was fixed before any roll was classified.
data-raw/height_calibrate-lower_tail_rolls.Rreproduces all of it, anddata-raw/flying_height_logbooks.csvis the transcription -
×10.764 is refused wherever a logbook reads the lower tail, and the third control suggests it is not the whole story on the upper side either: #54’s slipped roll
bc78065logs 1,350 ft where the catalogue holds 13,500, a decimal point rather than a unit conversion, and the planned-altitude rolls #54 repairs land on round feet under ÷10 but not ÷10.764. Filed as #71, not changed here. And the note’s claim that the mass beyond r ≈ 2 is a mislabelled lens, so nominal scale is right there, holds for about half of it: spacing puts 105 of its 209 sampled frames, identified per roll, at a wrong scale that the fallback draws at half width — #72
fly 0.14.1 (2026-09-21)
-
R CMD checknow runs in CI, on three platforms (#52)..github/workflows/held onlypkgdown.yaml, so a green PR check meant the docs site built — not that the package checks, and not that the test suite runs anywhere but the author’s machine. The new workflow runs on ubuntu, macOS and Windows at R release, on every push tomainand every PR, gating at the r-lib action’s defaulterror-on: "warning". No exported behaviour changes -
The platform axis is the one that earns the runner time.
flyreaches GDAL throughsf::gdal_utils()for warp, translate andnearblack, andfly_mask()asks for-alg floodfill— which GDAL’s ownnearblack.rstmarksversionadded:: 3.8. The three runners take GDAL from three different sources, andtest-fly_mask.Rsynthesizes its own images withterra, so it exercises that call offline on all three. Rdevelandoldrel-1are deliberately absent:flyis not going to CRAN, andoldrel-1is R 4.4, far above the declaredDepends: R (>= 4.1) -
A catalogue outage can no longer redden the gate, and that took a change to the suite rather than a claim about it. Seven blocks in
test-fly_georef.Rassertedexpect_true(result$success[1])on a fresh, unguarded download. Measured by routing HTTP at a dead port, the file as it stood gave FAIL 11; withskip_if_offline()+skip_if_not(all(fetched$success))— the pattern its own #49-era blocks already used — it gives FAIL 0, SKIP 9. Without that, a hiccup at the BC Data Catalogue would have reddened three runners on an unrelated diff, which is how a gate teaches people to ignore it. The same edit moves fourteen shared fixedtempdir()names towithr::local_tempdir():unlink()fails silently on Windows with a handle open, andfly_fetch()then serves the stale file as a cache hit -
A skipped test is now reported, because a skip is invisible in a green tick.
test-fly_camera_patb.Rcarries an unconditionalskip_on_ci()and the guardedtest-fly_georef.Rblocks skip when the catalogue is unreachable — so without a report, “the guards ran and passed” and “the guards never executed” produce the same tick. The action already cats the raw testthat output, so the step’s job is narrower than that: the skip block on its own rather than buried in a collapsed dump, and a failure when the output cannot be read at all, where the action’s version ends in|| true. All four of its branches were driven against fixture trees, including the one that must fail -
The one pre-existing WARNING is cleared here rather than gated around.
R/fly_mask.Rcarried a raw em dash in a warning message; it is now—, the remedyR CMD checknames and the formR/fly_footprint.RandR/fly_georef.Ralready use eleven times between them. The rendered message is byte-identical — verified against the parsed string value, sincedeparse()re-escapes. The alternative waserror-on: "error", which passes with a WARNING present: that is what makes it tempting and what makes it weak, since it would have passed over the next one too -
Five claims made along the way were wrong, and each is recorded as wrong rather than quietly re-pointed. Two predictions from planning: the non-ASCII WARNING names one file, not three — the probe that named three ran
deparse()over the parse tree, which renders—back as a literal em dash, so it measured its own un-escaping; and the predictedutils/statsundeclared-import NOTE does not exist, because they are base packages. Then three from a review round: aconcurrencygroup whose name carried no matrix value, so all three runners would have cancelled each other and read ascancelledrather thanfailure; a comment that invertederror-onsemantics; and a GDAL premise asserting>= 3.7— taken from this repo’s own conventions — which would have passed on a build with no-algflag at all. The convention error is filed as soul#255 -
A local check is not the CI check, which is worth knowing before reading either.
r-lib/actions/check-r-package@v2forces_R_CHECK_CRAN_INCOMING_=falseand_R_CHECK_FORCE_SUGGESTS_=false, so--as-cranhere never runs the CRAN-incoming URL check and the NOTE a bare localrcmdcheck()reports from it is not one this workflow will ever see. Measured under those env vars, the package is 0 errors, 0 warnings, 0 notes -
The first CI run found two pre-existing defects, neither reachable from this machine — which is the argument for the three-platform matrix, made by the matrix rather than by anyone’s reasoning about it.
test-fly_footprint.R’s anti-vacuity premise asserts the pre-#26 recomputed ring close still fails somewhere in a 720-bearing sweep; that is a property of the BLAS, true at 342 of 720 bearings here and 0 of 720 on all three runners, where asserting it took the whole check red. It now skips where the defect is unreachable and asserts where it is not, with the skip printed rather than silent. And Windows yields 2 bands for a masked grayscale frame against 1 with masking off, contradicting the unconditional “output band counts do not change” inCLAUDE.mdandinst/notes/border-masking.md— the stated reason masking could default to on without movingstac_airphoto_bc. Filed as #68, with a second Windows-only symptom from the same root: GDAL shifting genuine source zeros to 1 in the warped output, silently. The observed Windows band count is pinned rather than skipped, so nothing goes unasserted and the test reddens if that platform moves in either direction — including the direction where #68 is fixed
fly 0.14.0 (2026-09-20)
-
What a partially DEM-covered footprint costs is now measured, and it justifies one new column (#58). A footprint hanging off the edge of the DEM is sized from the mean of the part that is covered;
dem_coveragehas reported the fraction since 0.5.0 and the warning below 95% has fired since then, but nothing had measured what either was worth. 11,520 truncations of 120 real frames, cut from eight directions at eight depths by two mechanisms and compared against each frame’s own full-coverage answer.data-raw/dem_calibrate-coverage_error.Rreproduces all of it from public data, andinst/extdata/dem_coverage_sweep.csv,dem_coverage_targets.csvanddem_coverage_population.csvship so the suite recomputes every published table — all eight, cell by cell, at the precision each figure is printed to, with the table count itself asserted so a ninth cannot be added unchecked - New column
dem_shortfall_m: the metres the DEM would have to extend to contain each footprint, somax(fp$dem_shortfall_m)is the buffer that would contain the whole batch.0is an answer, not an absence — it means the DEM already spans the frame and the missing cells are nodata inside its extent, which no re-crop can recover. Those are the two causes of partial coverage anddem_coveragereports them identically; the warning now names which one you have and what to do about it. On the frame where coverage reads 0.253, this reads 3,822 m. Against a remote MRDEM read it is always0, because the window is fetched per frame — the shortfall is what a DEM cropped to an area of interest produces, which the population measurement says is the only way this happens in practice - New column
dem_elev_sd: the standard deviation in metres of the DEM values under the returned footprint, taken from valuesfly_dem_sample()already held and read off the same pass asdem_coverage. It exists because coverage cannot separate the frames inside a band. At a fixeddem_coveragethe measured error spans 13 to 52 times between frames — 47% covered over a plateau costs nothing, 80% across a valley wall costs a lot, exactly as the issue argued. Splitting each coverage band at its own median spread, the frames above it sit 2.3 to 4.1 times further from the full-coverage answer than those below. Read the pair together: coverage says how much is missing,dem_elev_sdsays how much that could be worth -
The error is exactly the elevation bias over the height above ground. Realised width error equals
Δelev / height_aglto 1.6e-13 over 10,248 runs, so the pipeline adds nothing measurable of its own and the result transfers past this package. In width, against each frame’s own fully covered answer: a median of +0.02% at 80-90% coverage, +0.04% at 60-80% and -0.01% below 20% — the median is near zero everywhere because truncation is unbiased in aggregate, and that is the trap. What grows as coverage falls is the spread: the 5-95% range is ±0.14% at 95-99% coverage and −7.3% to +5.5% below 20%, with a worst case of 23.6% -
fly_dem_coverage_min()stays at 0.95, and now has a measured reason. It was chosen to stay quiet on reprojection slivers. Measured, the worst linear error over every frame at or above 0.95 coverage is 0.794%, still 0.794% at 0.94, and first passes 1% at 0.92 — and one percent of width is about two percent of area, which is whatinst/notes/terrain-correction.mdrecords as the cost of the per-corner ray-casting this model defers. So 0.95 is very nearly the point at which a truncated frame’s worst case stops being smaller than an error already accepted in every frame. It is not moved down despite the median staying under 0.05% to 0.8 coverage: the warning’s own remedy, buffering past the corner, is correct at every coverage, so lowering it would silence the frames that advice works on -
No fallback floor was added, and that was a measurement. The rule was fixed before the numbers existed: add one only if some coverage band exists where the DEM route is worse than the nominal scale it replaced, in the median. There is none — even below 20% coverage the DEM route is better by nearly four times (1.49% against 5.76%), though it is worse on 15% of individual runs there. A frame sized from a sliver of its own terrain still beats one sized from a scale referenced to ground it does not cover.
no_dem_coverageremains the only fallback - Against the DEM this package recommends, partial coverage is all but absent. Of 1,437,147 DEM-eligible film frames, 113 could reach MRDEM-30’s nodata at all and 66 sit under 0.95 — 0.005%. Of 223,667 digital frames, 173 are candidates and none is partially covered: 167 of them never reach the DEM route, being sized from their ground sample distance. The control that licenses that census is a random draw of 3,000 frames, of which 0 were short of full coverage. The issue’s own figure does not reproduce: fly#58 reports 18 of 416 frames under 95% from the fly#50 run, and nothing in the catalogue’s digital population is partially covered by MRDEM-30. The likely explanation is a DEM cropped to that run’s area of interest — which is the failure the documentation already calls the ordinary one — but that is inference, and the run left no artifact to check
- The frequency is therefore not a property of the catalogue. With the default DEM it is a 0.005% event; with a DEM cropped to an AOI it happens exactly as often as that AOI is under-buffered, which no measurement here can bound. That is why the threshold rests on what truncation costs rather than on how often it occurs
- Three controls, each of which could have invalidated the result and did not. Mechanism:
NAmasking and extent cropping agree to a maximum of 0.43% over the 1,633 matched runs where both kept the frame on the DEM route; they diverge on three runs, all at total loss, where one cell ring decides betweendem_aglandno_dem_coverage. Grid: the same frames swept at ~900 m, reprojected to EPSG:4326 and with anisotropic cells give median errors of 0.696%, 0.585% and 0.677% below 0.8 coverage against 0.777% natively, each against its own full-coverage reference — the geographic arm matters most, because the reprojection branch 0.95 was chosen for does not execute at all on a DEM in the data’s own CRS. Height checks: 873 of 7,616 runs change classification and every one isno_dem_coverageat coverage exactly 0, so a biased first pass never pushes a frame acrossfly_height_ratio_band() -
A calibrated error bound is deliberately not shipped.
4.97 * dem_elev_sd * (1 - dem_coverage) / height_aglcovers 95.3% of held-out runs, fitted on the training third alone — but it is a median 6.1 times the actual error and holds on only 89.4% of runs below half coverage, which is where a caller would want it. A number that looks authoritative and is six times too large is worse than the two inputs it was built from -
What the sweep is blind to, stated rather than left implicit. MRDEM returns a near-zero surface over near-shore ocean rather than nodata — 40,000 cells in Hecate Strait read 0.098 to 0.189 m, with no exact zeros — so a coastal frame reports coverage near 1 while its mean is dragged toward sea level. That is a coverage-1 failure, removing cells cannot generate it, and
dem_coveragecannot see it. Note also that the obvious guard for it, counting exact-zero cells, can never fire
fly 0.13.0 (2026-09-20)
-
A
/vsicurl/DEM is now practical: the reported two-frame case goes from 263 s to 4.3 s (#59).fly_dem_sample()calledterra::extract(dem, v)once over every rectangle with nofun, which returns every cell — and over HTTP that read cost 64.59 s and 158 range requests for a single 1:15000 frame against MRDEM-30, where the same frame read through a crop of its own window takes 0.69 s and 4 requests. The DEM is now read one window per frame, and the same two frames come back with identicaldem_coverage,height_aglandfootprint_terrain -
The window was chosen by measurement, and two plausible alternatives are wrong. Timing one form per fresh R process, so the GDAL block cache cannot carry between them,
fun = meanis exactly as quick (0.66 s) — but the coverage numerator counts non-NAcells and an aggregating extract returns one number per frame, so that route needs a second call with a custom closure terra applies in R over the values it had to return anyway. And cropping once to the whole batch is wrong form 4 ofdem_coveragearriving through the read:fly_dem_sample()is handed every rectangle at once, so a crop spanning them is sized to the gap between photos — 243,583,754 cells for the two frames 700 km apart in this package’s own suite. Bounding the crop to one footprint costs 23% on contiguous frames (1.23 s against 1.00 s for eight, the block cache absorbing their 60% overlap) and cannot produce it. The local-DEM path does not regress either — 1.260 s against 1.437 s for the 20 bundled frames, because each extract now works over a window of a few thousand cells instead of the whole raster -
The read window is not the counting template.
fly_dem_grid()align()s with terra’s defaultsnap = "near", which moves each edge to the nearest cell boundary, so the template can be smaller than the footprint — and a first version of this change read through it, losing cells the whole-DEM extract returned. A review round measured 52 of 300 frames drawn between 0.2 and 6 cells across disagreeing on mean elevation. Frame width is not the condition — interior frames 3.8 cells across diverge 0 of 200 times, because a near-snap only discards a column whose centre is already outside the polygon. What triggers it is the frame’s overlap with the DEM covering no cell centre, which happens to a frame of any size at the edge of coverage — the ordinary AOI-cropped DEM. With the defect restored, 100 of 100 overlap depths under half a cell diverge and 0 of 100 once the overlap passes a cell. The read is snapped out (a superset of both the footprint and the template) while the template keepssnap = "near", because fly#9 measureddem_coverageagainst that grid. Caught in review. The regression test drives twelve full-size frames overlapping a 900 m DEM’s edge by under half a cell, checks each against the whole-DEM extract, and takes the trigger condition itself as its premise — every frame’s overlap must cover no cell centre, computed against the cell centres directly. It fails 12 of 12 with the defect restored - A new test records every extent handed to
terra::crop()and bounds it; planting a union-extent crop takes it to the 243,583,754 cells above. The pre-existing grid assertion stays green through that plant — it watchesfly_dem_grid(), which is still per-frame, and cannot see the read at all — so the new test is not redundant with it -
One new failure mode, guarded.
terra::extract()returns anNAplaceholder row for a rectangle that misses the raster entirely;terra::crop()errors on it. Unguarded, one unlocatable frame would abort a whole batch — which is precisely what the per-frame reporting in 0.4.0 exists to prevent. Removing the guard errors five tests, three of them pre-existing -
What licensed the change was parity, not the timings. The previous implementation was pulled from git rather than rewritten, and returns identical
elevandcoveredover eleven shapes: all 20 bundled frames, a frame 200 km off the DEM, two off and two on, a frame straddling the DEM edge, an empty geometry among real ones, all-empty input, a DEM hole, a geographic-CRS DEM, anisotropic 120 x 904 cells, a multi-layer DEM, and frames swept from 0.05 to 60 cells across at three resolutions. The parity is conditional and the note states the condition: it holds wherever a frame covers at least one cell centre. A frame that covers none but still touches the raster takesterra::extract()’s touched-cells fallback, which is computed over whatever raster it is handed, so a crop and the full DEM can return different cells — measured on a synthetic 10 x 10 grid, 96 against 95.5, withdem_coverageunchanged. There is one deliberate behaviour change in the same family, and it is a correction rather than a loss: a frame that merely abuts the DEM shares a boundary line with it and no area, so no cell centre can lie inside it — the old code still returned a mean from the cells touching that line (elev60,dem_coverage0.0556 on that grid) and the new code reports no elevation and zero coverage, which is whatno_dem_coveragemeans. No route to any of this was found throughfly_footprint().terra::align()also returns the same extent whether handed the DEM or a crop of it, andterra::crop()to an overhanging extent clips rather than pads — so ground past the edge still yields no row, which is wrong form 1 - The issue’s headline figure of 583 s was measured against a link shared with the 0.12.0 calibration sweep and is an upper bound; 263 s is the same case on a quiet link. See the new section of
inst/notes/terrain-correction.md
fly 0.12.0 (2026-09-18)
-
fly_footprint(dem = )no longer believes aflying_heightits own scale contradicts, and repairs the one error it can identify (#54). fly#54 reported two 2003 frames 110,834 m across with a DEM against 8,022 m without. Nothing in fly was wrong: the catalogue’sFLYING_HEIGHTis 3.28084² = 10.764 times too large on 1,589 film frames across 13 rolls (1974-2005, 1,054 of them on five rolls of 2003) — a feet-to-metres conversion applied the wrong way round. Measured over all 1,670,471 centroids, reconciled against the catalogue’s own count, and over MRDEM-30 under the 7,156 frames that decide the constants. Divided by the factor, the 1,589 land at 0.80-1.32 of the height their scale implies, against 10.0-15.8 as published, with nothing between 6.69 and 10.01; read as plain feet instead they sit at 3.0-4.7 and not one lands in the band - New column
height_source:"reported","corrected_unit_slip"(sized fromflying_height / 10.764;height_aglis the height used),"implausible"(the height disagrees withscale x focal_lengthby more than 1.6x either way and the slip does not explain it, or is above 16,000 m, or is below the terrain — sized from nominal scale instead), andNAwhere no height was judged. Yourflying_heightcolumn is never overwritten, so on a corrected rowflying_height - height_aglis not the ground elevation.dplyr::filter(fp, height_source != "reported")lists the frames worth a second look -
The check had to be relative, which is not what the issue proposed. A plausibility bound on
height_aglcannot work: slipped rollbc78065reads 4,115 m at 1:2000 and the highest legitimate height in the catalogue is 14,630 m, so no bound separates them. A film frame states its height above ground twice —flying_heightminus terrain, and scale times focal length — and those are compared. The 16,000 m ceiling that remains is a backstop for digital frames, whosescaleis not an image scale; no digital frame in the catalogue comes near it (highest 7,513 m of 223,667) -
A frame outside the band now falls back to nominal scale where it used to be sized from the DEM, which changes footprints for at most an estimated 1.2% of film frames (fewer, since a frame whose terrain sits above its recorded height was already falling back): 98.7% sit inside the band, weighting the sampled strata by the population they stand for, and the slipped 0.1% are repaired rather than refused. For the one group whose cause is known, falling back is the correct answer and not merely the cautious one: the mass centred on r = 2 is a 305 mm lens catalogued as 153 (209 of the 223 frames sampled beyond r 1.8 in that stratum), which the DEM route drew at twice its true width and the nominal route gets right because it never reads
focal_length. For the rest,rcannot tell a wrong height from a wrongscale, which is why they are flagged rather than silently resized. Every bundled frame is inside the band and unchanged -
The slip also appears to run the other way, and that is deliberately not repaired. Of the 1,962 sampled frames reading under half their nominal height (1,963 in the catalogue), multiplying by 10.764 brings 726 into the band — but multiplying by 10 brings 729 and doubling brings 799. Three remedies the terrain cannot tell apart, so none is applied; they are flagged
"implausible"and tracked in #60 -
No DEM window is built from a height that fails the checks.
mainsampled its second pass over the rectangle the bad height implied — 110 km of terrain per frame. A refused frame is withheld from the second pass, and a camera-table frame over the ceiling is never seeded at all; a test asserts on the grids rather than on the answer, since classifying after both passes returns the right footprint and still pays for the wrong one - The rule is per frame, never per roll: 1,208 frames sit on a slipped roll without being slipped. Measured over the catalogue, the repair fires on the 1,589 and on none of the 2,733 other sampled frames outside the band
-
data-raw/height_calibrate-flying_height_slip.Rreproduces every figure above from public data, and the sweep ships asinst/extdata/flying_height_sweep.csvandflying_height_population.csvso the suite checks the three constants against the data rather than against themselves. See the new section ofinst/notes/terrain-correction.md - An existing test relabelled a 1:12000 frame as 1:31680 without moving its
flying_height, building exactly the disagreement this release refuses — and had been sizing its “wide frame” at 2.7 km. It now uses a frame that is 1:31680, which comes back 7.7 km - Split out while here: what a partially DEM-covered footprint costs (#58), and
fly_dem_sample()taking 583 s for two frames over a/vsicurl/DEM (#59)
fly 0.11.0 (2026-09-08)
-
The black collar around a scanned frame is now masked properly, and fly#23’s premise that it is a circle was wrong (#23). The issue asked for Hough-transform detection of a “circular lens image area”. Measured over all 264 thumbnails in the
stac_airphoto_bccorpus (1967-2018), an inscribed circle implies a dark fraction of 0.2146 with bright edge midpoints; the measured median is 0.027, only 2 of 264 frames land near 0.2146, and the edge midpoints are as dark as the corners — which no circle can produce, since a circle inscribed in the frame passes through the middle of each edge. It is a rectangular collar with chamfered corners, and 27 frames carry none at all. There is also a physical argument the thumbnail statistics cannot supply: a mapping camera’s format is built to sit inside the lens’s image circle, so the circle circumscribes the format and there is no dark circle at any resolution -
srcnodata = "0"was masking almost none of the collar, which is the mosaic symptom fly#22 and fly#23 both describe. It matches exact zeros and scanned black runs 3-12: median mask fraction 0.0016 against 0.0311 at threshold 16, a 19.8x gap, with 128 of 264 frames having under a tenth of their collar masked and some none at all (bc88011_200_12nmeasures 0.0000 against 0.0176). Both halves of the tradeoff the old roxygen stated were therefore false: it did not mask the borders, and the “real black pixels” it was said to cost are a median 0.16% of a frame and are collar rather than shadow - New exported
fly_mask()masks the collar with a flood fill seeded from the image border, so only darkness reachable from the edge is masked and interior dark water survives. On 1990 frame bcb90128_213 a plain threshold masks 7.7% of the frame and this masks 2.5%; the missing 5% is a lake. That distinction is the whole point — fly#23’s own words were “nodata=0 loses real dark pixels” -
Measured end to end against the old path over one thumbnail per year: of 18 grayscale frames spanning 1967-2000, 16 lose more collar, none lose less, and the two unchanged are exactly the frames that have no collar. 602,219 pixels of collar that the old path left in the mosaic.
bc88011_200_12ngoes from 33 masked pixels to 28,448;bcb96027_004from 690 to 65,756. On RGB, a 2004 frame loses 4.9% of its opaque pixels and a 2018 digital frame with no collar loses 0.0% -
No new dependency. GDAL’s
nearblack -alg floodfillis this algorithm andsf::gdal_utils()already dispatches to it. Aterra::patches(directions = 8)implementation was written first and measured against it over all 264 frames — correlation 0.9877, 0 of 264 disagreeing by more than 0.02 — then kept indata-raw/as the control rather than shipped, because it would put a Suggests package on a default code path and read a 9600 x 9000 image into R.terrastays in Suggests -
Nothing downstream changes band count.
-srcalphaforces the last band to be alpha and excludes it from the warped band list, so RGB still yields 4 bands and grayscale still yields 1 band withnodata = 0. The GCP step is now a VRT, which carries a<GCPList>gdalwarp reads, so masking removes a full-size temp copy rather than adding one -
fly_georef(mask = )is new and defaults to"border";srcnodatanow defaults toNULLand passing it alongside the mask is an error. GDAL accepts the combination silently, which is exactly why the package refuses it: measured on a synthetic frame carrying an 11 x 11 block of true black, adding-srcnodata "0 0 0"to the masked warp deletes precisely those 121 interior pixels, reintroducing the loss the mask exists to prevent with nothing reported.mask = "none"reproduces the pre-0.11.0 option vector byte for byte, and a test asserts that against a hardcoded copy of it - Both constants are measured, and the first two readings of the sweep were wrong.
fly_mask_threshold()is 16, taken from the per-frame threshold at which a frame’s own collar is fully consumed (quantiles 50% 8, 90% 12, 99% 16, max 16) — not from the population median of mask fractions, whose smallest increment is at 24 -> 32 and which cannot separate a fully-removed collar from a small one anyway.fly_mask_max_interior()is 0.05 on the interior fraction rather than the total, because total area cannot tell a thick collar from a lake running off the edge. An earlier draft published an admissible band of (0.0131, 0.2379) with 4.8x margin; 0.2379 is the maximum interior fraction at threshold 48, not the smallest that trips the cap there, which is 0.0510. The two constants are coupled — the worst legitimate interior fraction reaches 0.0465 by threshold 32, where the cap clears it by 1.08x — so raising the threshold alone breaks the guard. The sweep behind both ships asinst/extdata/mask_border_sweep.csvand the test suite recomputes every published figure from it - When the guard trips the frame is left unmasked with a warning, not skipped — deliberately the opposite call to the stretch tolerance in
fly_georef(), because the outcomes are asymmetric here: an unmasked frame shows a black border, which is visible and is what every caller lived with before this release, while an over-eager mask deletes real imagery transparently. A mask fraction of zero is not a warning; 27 of 264 frames legitimately have no collar. A source that is not 8-bit is refused rather than reported as having no collar —-nearis an absolute per-band distance, so a Byte-calibrated threshold reaches nothing on a 16-bit scan and the two states would otherwise share one representation -
Full-resolution scans remain unreachable from this package — the catalogue centroid layer carries no URL for them and they are licence-restricted — so fly#23’s stated trigger cannot be fixtured, tested or calibrated here, and no frame in the measured population can trip the runaway guard at threshold 16. Only a synthesized fixture tests it, and the suite says so.
fly_mask()is exported precisely so a caller holding licensed scans on disk has an entry point that does not go throughfly_fetch(). Seeinst/notes/border-masking.md
fly 0.10.0 (2026-09-06)
-
A digital frame with no calibration report can now be sized from the camera the province names in its PAT-B file (#50). 41,249 frames across 2011, 2012, 2015 and 2016 carry a
patb_georef_urland nocamera_calibration_url; until now they were sized only fromfocal_length, which fixes sensor width to a few percent and says nothing about pixel count, sofootprint_basisreadinferred_formatand the frame got no footprint at all without a DEM. New exportedfly_camera_patb()fetches and parses those georeferencing files and attachescamera_serial,camera_name,patb_gsdandpatb_source;fly_footprint()picks the columns up and sizes the frame on the ordinarypixel count x GSDroute, needing no DEM. 22,366 of the 41,249 resolve — 4,822 UltraCam Xp at 5,193 m, 2,827 UltraCam X at 4,329 m, 14,717 UltraCam Eagle at 5,002 m — and the other 18,883 are refused with a reason inwidth_sourcerather than guessed at -
fly_camera_patb()is the only function in the package that reaches the network on the sizing path.fly_footprint()still fetches nothing, so a batch is resolved once and reused acrossfly_coverage(),fly_select(),fly_overlap()andfly_georef() -
The catalogue’s
ground_sample_distanceis 0 for whole years of digital imagery — measured, all 24,742 frames of 2011 and 2012 — so resolving the camera alone would have left this issue’s own headline case unsizeable. The PAT-B files carry the number, andpatb_gsdis used only where the catalogue has nothing; where both are present they agree, and preferring the file would make a footprint depend on whether the caller had run the fetch -
A camera identity the shipped table does not recognise is refused, not resolved from the model string beside it. One published archive labels both its cameras
UltraCam XPand one of them is serial70912643— aUC-SXUltraCam X, 14430 px against the Xp’s 17310 — so a name fallback would size 1,790 frames 20% wide. The model string is read only where the file carries no serial at all (lens_nois 0 on every row of both 2012 archives), and then only on exact equality: the publishedDMC II 230must not be prefix-matched to the shippedDMC IIrow, which would also size a DMC II 250 or 140 from it -
camera_formats.csvshipped70912643_2015mislabelled. Its serial isUC-SX-1-70912643and its dimensions are an UltraCam X; thecamerafield saidUltraCamXp. Only the label was wrong and nothing joined on it, which is why it survived — this release makes it a join key. The generator now reads the model from the serial prefix rather than scanning the report prose, whereUltraCamXpsat six entries aboveUltraCam Xin the pattern list, and a test asserts every calibration-row label maps to exactly one sensor size -
Behaviour change for
demcallers, and it is a small one. A newly resolved frame becomes GSD-sized, and the DEM route is!by_gsd, so passing ademnow returns the GSD footprint where it used to return a terrain-corrected one. Measured against the exterior orientation the province publishes per frame (sensor width x agl / focal) over every row of the three archives — 4,822 UltraCam Xp, 2,827 UltraCam X, 15,592 UltraCam Eagle: 5,193 m against 5,220 m, 4,329 m against 4,337 m and 5,002 m against 5,000 m, ratios of 0.995, 0.998 and 1.001. The stated GSD is nominal but the implied one differs from it by 0.2 cm at most on these frames, so the GSD-beats-DEM precedence set in 0.6.0 stands and the residual is under 0.5% - A frame can now gain a footprint and still gain no GeoTIFF.
fly_georef()skips a frame whose delivered image aspect does not pair with its footprint edges, and its constant was measured on the two cameras in the bundled fixture only. All three newly reachable bodies were measured against the live thumbnails for this release and deliver portrait exactly as those two — UltraCam Xp and X at 784 x 1200, UltraCam Eagle at 818 x 1251, matching their shipped aspects to 0.2% — so they pass the gate. The quarter turn remains geometrically unsettleable; seeinst/notes/georeferencing.md -
fly_fetch()no longer treats a zero-byte file already indest_diras a completed download.download.file()truncates its target before it runs, so an interrupted fetch left a 0-byte file that the existence check then blessed forever, and a caller reading it saw an absence rather than a failure.data-raw/make_camera_formats.Rhas carried this guard since 0.6.0;fly_fetch()did not -
inst/notes/camera-formats.mdrecords the three PAT-B schemas, why the serial index runs two passes rather than one union (a union makes serial20814295ambiguous and refuses 14,717 correct frames), and why the20814295/22814295gap in the table is the catalogue’s own filing rather than a typo
fly 0.9.0 (2026-09-02)
-
Every footprint is now rotated onto its flight line, so diagonal film footprints move (#26). v0.6.0 built continuous rotation and gated it on the recording format being non-square, which kept film byte-identical. That gate was wrong about the ground: a film camera is mounted square-on to the flight line just as a digital one is, so the real footprint is a square rotated onto the bearing. An axis-aligned square is correct only on a cardinal heading, and at 45 degrees it overlaps the true footprint by
2(sqrt(2)-1)= 83% —fly_coverage()was reporting that missing sixth as covered ground. A newfootprint_bearingcolumn records the azimuth each ring was rotated onto, orNAwhere it was drawn axis-aligned -
fly_bearing()now refuses a neighbour that is not adjacent byframe_number, which is breaking for sampled input. It used to take the azimuth to the next frame present in the object handed to it, with no test on gap or distance. A roll flies several legs, so in a sample that pairs frames across a turn: bundled frames bc5282 179 and 199 are 20 apart and 3.3 footprint-sides apart, and give 59.8 degrees on a roll flying about 230. That was survivable while nothing consumed the bearing and largely invisible when v0.6.0 applied it to digital only; rotating film makes it rotate a footprint bodily. It also made geometry batch-dependent —centroids[1:2, ]returned 51.5 for both frames where the full 20-row batch returns 318.2 and 231.6, so the same photo covered different ground depending on the subset it was passed in. Seven of the twenty bundled frames keep a bearing. To hold bearings across a subset, callfly_bearing()on the contiguous roll first and carry the column -
fly_georef()refuses a rotated film frame unless you supply the roll’s rotation, and this is a measurement rather than an omission. Adjacent-frame overlap correlation over four contiguous legs gives 0 for bc5282 (1968) and 90 for bc83062 (1983), the latter consistently at three widely separated bearings. So the mapping is genuinely flight-relative — a geographic convention could not return one answer across three bearings, and a fixed-geographic rival was tested and falsified — but it differs by a quarter turn between eras, which is what #26 said at the outset. There is no film equivalent of the digital constant to write. A wrong mapping here yields a valid GeoTIFF over the right ground with the picture turned 90 degrees, which nothing downstream would report, so the frame is skipped with a warning naming the roll and the fix. Set arotationcolumn onphotos_sfonce you have checked one frame of the roll against known ground -
The
rotationcolumn’s meaning changed. It used to shift corners on an axis-aligned square; it now shifts them on a ring already rotated onto the bearing. A value calibrated by eye against an earlier release no longer means the same thing and must be re-checked. Therotationargument likewise now only ever reaches frames for which no bearing could be computed -
fly_georef()routes onis.finite(footprint_bearing)rather than onfly_is_square(). Squareness stood in for “was this ring rotated” and the two have come apart now that film is rotated too - Fixed a latent defect in
fly_rectangles()present since v0.6.0 for every rotated digital frame: the closing ring vertex was recomputed by the rotation rather than copied, and%*%computes rows independently, so an optimised BLAS can return it a few ulps off the first (measured 2.8e-14 at bearing 230).sf::st_polygon()requires exact closure and errors, so one unlucky frame aborted the whole batch. Data-dependent, which is why the bundled fixture never triggered it - The no-bearing warning was gated on the footprint being non-square and so never fired for film, which needs it as much as digital does
fly 0.8.0 (2026-09-01)
-
Every function that takes photo centroids now refuses non-POINT geometry (#37).
sf::st_coordinates()returns one row per feature for a POINT and one row per vertex for anything else, sofly_footprint()handed its own output built five geometries per closed rectangle while the attribute frame stayed at 20 rows, andst_sf()recycled it up to match: 20 frames in, 100 rows out, no error and no warning, 80 of them carrying another photo’s attributes - The reported symptom was the row count, which is the detectable half. Measured on the bundled fixtures, three functions were quietly wrong rather than wrong-sized:
fly_bearing()returned the right number of bearings with values up to 272 degrees out, because it indexes a 5n-row coordinate matrix with a permutation of1:n;fly_overlap()reported pairwise overlap across the corrupted set; andfly_select()in both modes indexed a 20-row frame with a length-100 logical.fly_filter(method = "centroid")never reachedfly_footprint()at all and silently became a footprint filter, selecting 20 rows on the bundled AOI where points give 7. Onlyfly_coverage()errored, and only by accident of how it assigns - The guard is POINT only, not
c("POINT", "MULTIPOINT")as the issue proposed. A MULTIPOINT expands one row per constituent point exactly as a POLYGON does — measured: under the suggested guard the same footprints cast to MULTIPOINT still returned 100 rows from 20. POINT is not a proxy for “one coordinate row per feature” but exactly equivalent to it, sincesfkeeps an alignedNArow for an empty POINT; zero-row input stays legal - The error names the argument the caller actually typed rather than
fly_footprint()’s, names the offending geometry type, and names the one-line fix —sf::st_filter()for someone who meant to filter footprints against an area,sf::st_cast()for centroids that arrived in another form -
Breaking for two input shapes that previously worked. Centroids arriving as MULTIPOINT-of-one — from a PostGIS
MULTIPOINTcolumn, or an OGR driver that promotes to multi — were sized correctly before and are now refused; so is a mixed-geometry (GEOMETRY) column, even when every feature in it is a point, becausesf::st_coordinates()has no method for that class and the caller otherwise sawNot compatible with STRSXP: [type=NULL]several layers down. Both are one call to fix, which the error names:sf::st_cast(x, "POINT")
fly 0.7.1 (2026-09-01)
-
fly_footprint()’s@param demnames the LidarBC STAC catalogue asstac-elevation-bc, its current name (#46). Documentation only —flynever queries that catalogue, it takes a COG URL the caller already has, and the S3 bucket kept its old name, so no href in any example changes
fly 0.7.0 (2026-08-30)
-
fly_georef()georeferences digital frames (#38). v0.6.0 gave them footprints and then excluded them from georeferencing with a warning; that exclusion is gone, so the whole post-2010 catalogue is now georeferenceable rather than only sizeable -
The corner mapping for a non-square footprint is rotation 270 — the top-left pixel maps to the ring’s rear-left corner, equivalently image columns run in the flight direction and image rows run flight-right. It was measured, not reasoned: a digital footprint is already rotated onto its flight line, so
bearing_to_rotation()is not applied to it, and the geometry that remains cannot distinguish 270 from 90 - The measurement needed no licence-restricted imagery, which the issue had assumed it would. Three independent public routes agree: the per-frame exterior orientation the catalogue publishes through
patb_georef_url(the UltraCam Eagle’s mount is rigid to 0.18 degrees over 6839 frames spanning the compass); adjacent-frame overlap correlation, which needs no reference imagery at all because consecutive frames check each other (+0.616 and +0.659 against at most +0.43 for the alternatives); and FWA lake darkness.data-raw/georef_calibrate-corner_mapping.Rreproduces all three, andinst/notes/georeferencing.mdrecords them — including the one that disagreed and was wrong - A frame whose delivered image aspect does not pair with its footprint edges is now skipped with a warning rather than written stretched. A wrong pairing produces a valid GeoTIFF in the right CRS over the right ground, squashed by the aspect ratio squared, which nothing downstream would report. This also catches a frame sized from an inferred camera format that does not match the camera that took it
- A non-square footprint built without a flight bearing is drawn axis-aligned and so georeferences as though the flight line ran due north.
fly_bearing()needs a neighbouring frame, so this is the ordinary result of georeferencing one frame on its own, and it is now warned about rather than left to be noticed in the output - The
rotationargument applies to square footprints only, and is documented as such; arotationcolumn inphotos_sfstill overrides per-photo for both. Carrying a film-erarotationcolumn into a digital batch therefore overrides the correct mapping — drop the column, or set it toNAfor those rows
fly 0.6.0 (2026-08-30)
-
fly_footprint()now sizes digital frames, closing the gap #30 made honest but left open (#32). Province-wide that is 223,667 of 1,670,471 frames — the package was quietly film-only for anything after ~2010 - Sensor dimensions are read from the camera calibration reports the catalogue itself links to through
camera_calibration_url, and shipped asinst/extdata/camera_formats.csv(built bydata-raw/make_camera_formats.R). 14 calibrations covering 169,688 frames, plus focal-length fallback rows for frames carrying no calibration -
The catalogue’s
SCALEis not the true image scale for a digital frame, and is no longer used for one. Measured against terrain on 40 UltraCam Eagle frames it gives 34% of true width: it is a derived nominal figure, implying a pixel pitch of ~12.5 um for every camera regardless of model against real pitches of 3.9-12 um. A digital frame is sized aspixel count x ground_sample_distanceinstead, which needs neitherscalenor a DEM.ground_sample_distanceis centimetres -
Footprints are no longer always square. Digital sensors run from 1.10:1 (Leica DMC II) to 1.80:1 (Intergraph DMC), so a square footprint was up to 76% too deep. Non-square footprints are rotated onto the flight line via
fly_bearing(). Film stays square and its output is unchanged - New
width_sourcecolumn names the calibration file or fallback rule behind every digital footprint, andfootprint_terraingains"gsd_scaled"—nominal_scaleis documented as “sized from the reported scale”, which is the one thing this route never does - Frames whose calibration could not be corroborated are refused rather than inferred, listed with the reason in
inst/extdata/camera_formats_excluded.csv. Two are medium-format bodies about half the width of everything else in the record, so inferring one from focal length would have been ~1.95x too wide -
fly_georef()excludes rotated footprints with a warning: its corner mapping applies its own bearing rotation, calibrated for axis-aligned squares, and would count the rotation twice - The shipped numbers are parsed from the reports, never typed, and gated on four checks before the table is written —
px x pitchagainst the stated image size, report focal against the catalogue’s, plausibility bounds, and an implied ground elevation that must be a real BC elevation. The last two caught a camera whose catalogue metadata contradicts its own report, which is withheld -
format_sizeis unchanged and still takes precedence, so a caller who knows their camera can override the shipped table - New
inst/testdata/photo_centroids_digital.gpkg: 24 real digital frames over the AOI that already ships, from two cameras 0.46 apart in aspect ratio
fly 0.5.1 (2026-08-29)
- Fix
fly_footprint()silently droppingfootprint_basis,footprint_terrain,height_aglanddem_coveragewhenever its input carried thetbl_dfclass (#35).bcdata::collect()returns exactly that class, so every caller queryingWHSE_IMAGERY_AND_BASE_MAPS.AIMG_PHOTO_CENTROIDS_SP— the documented source for this package — lost the whole reporting surface 0.4.0 and 0.5.0 added, and the documented “filter onfootprint_basis” and “filter ondem_coverage” workflows were unreachable from it - Geometry and every downstream number were always correct; what was lost was the audit trail, which is what made it invisible.
sf::st_sf()keeps only its first argument when that argument is a tibble, discarding every trailing named column — a general R trap rather than aflyone - Every class the input carries is carried through, so a tibble-backed sf comes back tibble-backed. The order is not preserved:
sf::st_transform()movessfto the front, so abcdc_sfinput returnssf, bcdc_sf, ...— as it always has - Where the input already carried a column named
footprint_basis(or one of the other three), the old code kept the caller’s value and appended a duplicatefootprint_basis.1— so the documentedfootprint_basis != "unknown_format"filter read the caller’s column while fly’s own answer sat unread beside it. The computed value now wins, and there is one column. Plainsfcallers were affected by this too, though never by the tibble bug -
fly_footprint()on an input matching no frames now returnsfootprint_basisandfootprint_terrainascharacterrather thanlogical, so an empty result binds to a populated one. Assembling a per-AOI ledger across queries previously failed on the column type the first time an area returned nothing - Tests sweep the input-class axis (plain / tibble / grouped) rather than adding cases along the one axis the bundled fixture could present. Every fixture in the package reads back as plain
sf, data.frame, so the suite was structurally incapable of seeing this - Widen the
DESCRIPTIONTitle and Description, which described roughly half the package — they predatedfly_fetch(),fly_georef()andfly_bearing()(#31)
fly 0.5.0 (2026-08-29)
-
fly_footprint()gains ademargument, sizing each frame from its height above ground instead of the reported scale (#9). On the bundled Upper Bulkley AOI the reported scale understates footprint area by a median 14%, ranging to 26% — and always in the same direction, because the scale is referenced to an elevation above the valley floor the photos cover. This is a datum offset, not the slope effect the issue described -
FLYING_HEIGHTis metres above sea level, so subtracting terrain elevation is what turns it into the height ground coverage scales with. Elevation is the mean under the whole footprint, not a reading at the centroid — the two differ by up to 140 m on a 7.2 km frame. It is measured in two passes, because the footprint being averaged over is itself what the correction changes - New
footprint_terrain,height_aglanddem_coveragecolumns record which terrain treatment each frame received, the height it was sized from, and how much of its footprint the DEM actually covered — measured against the cells the footprint should have covered, so that a footprint running past the edge of a cropped DEM is reported rather than counted as complete.footprint_basisis unchanged: it is already matched by value downstream, so encoding terrain into it would break caller filters -
demis accepted byfly_coverage(),fly_overlap(),fly_filter(),fly_select()andfly_georef(), so the correction is reachable from every function that builds a footprint - Frames the DEM cannot correct — outside its coverage, or with unusable
flying_height/focal_length— fall back to nominal scale with a warning rather than being dropped. A frame the DEM covers only partly is still corrected, from the mean of the covered part, and warns below 95% coverage - New
inst/testdata/dem.tif, a clip of NRCan’s MRDEM-30.terraadded to Suggests; it is only needed when ademis supplied - Footprints remain axis-aligned rectangles under a nadir assumption. Per-corner ray-casting measures ~2% against the 14% the DEM addresses, and is deferred
fly 0.4.0 (2026-08-28)
-
fly_footprint()sizes each frame from itsmediavalue instead of applying a fixed 9-inch negative to everything. A digital frame has no negative, and the catalogue mixes film and digital in one layer (#30) - New
footprint_basiscolumn records how each footprint was sized. Frames whose recording format cannot be resolved get an empty geometry and a warning, rather than a plausible rectangle - New
format_sizeargument supplies widths for formats fly does not ship. Digital defaults are deliberately absent until sensor widths are established (#32) -
negative_sizekeeps its meaning as the film dimension: it sizes film and no-media input, and never sizes a digital frame - All functions that consume footprints now report how many frames they excluded for want of one
- Fix
fly_coverage()dropping an entire group when all its footprints were unsized, and emitting multiple rows for a multi-feature intersection - Correct the documented claim that focal length is unavailable in centroid data —
FOCAL_LENGTH,FLYING_HEIGHTandSCALEare fully populated
fly 0.3.0 (2026-03-12)
-
BREAKING: Rename
fly_thumb_georef()→fly_georef()— not thumbnail-specific (#24) - Add
rotationparameter tofly_georef()(default 180°) for correcting image orientation per-roll (#25) - Add
fly_bearing()— compute flight line bearing from consecutive centroids per roll - Per-photo rotation via
rotationcolumn on input sf overrides default
fly 0.2.1 (2026-03-11)
- Add
workersparameter tofly_fetch()for parallel downloads viafurrr/future(#21) - Add
furrrandfutureto Suggests
fly 0.1.3 (2026-03-10)
- Add
fly_thumb_georef()— warp downloaded thumbnails to estimated ground footprints as georeferenced GeoTIFFs (#16)
fly 0.1.2 (2026-03-10)
- Add
fly_fetch()for downloading thumbnails, flight logs, calibration reports, and georef files from BC Data Catalogue URLs (#15) - Include URL columns and flight metadata (focal length, flying height, GSD) in bundled test data
fly 0.1.1 (2026-03-07)
- Add
component_ensureparameter tofly_select()for multi-polygon AOIs — guarantees at least one photo per disconnected component before greedy selection (#12) - Vignette uses bookdown with numbered sections and figure cross-references
- Add
bookdownto Suggests
fly 0.1.0 (2026-03-04)
Initial release. Airphoto footprint estimation and coverage selection, extracted from airbc.
