Categories
Uncategorized

Today Ollie is initiating launch sequence

We’re helping him get set up in residence at Carleton res. Looks like all systems are go…

Categories
Software

Camera1 + Camera2 = Lua

I recently used two cameras to take photos at a soccer game. One with a moderately zoom-y lens (80-200mm) the other with a more zoom-y lens (200-500mm).

Rovers vs Altitude FC
August 4, 2024 (exact time TBD) Rovers vs Altitude FC, Swangard Stadium, Burnaby

When I was looking through the uploaded photos I noticed the time stamps of the two cameras didn’t line up. I’ve seen this before when I have some photos from my phone (which is very smart with time zones and daylight savings) and other photos from an SLR. (much less smart with time zones and daylight savings) Lightroom is very good about letting you select the photos from one camera and offsetting their capture times by a specific amount.

But this is where I made a strategic blunder! I adjusted camera 1 to match the clock of camera 2. Next I went to Lightroom to adjust the times of the camera1 photos. This would have been easy if I knew the delta between the clocks in the two cameras. Sadly I had just destroyed that evidence (by synching the camera clocks.)

So I tried estimating, which was close, but there were a few places where photos from both cameras now had the same capture time, and I was unable to judge from the content whether I needed to increase or decrease the compensation.

I was fairly confident that if I could present the capture times visually (as two data series in a graph) it would be obvious how much I’d need to shift one series to sync with the other series. If only I could extract all the time stamps from both sets of photos, and present them in a graph.

But how do I get all the time stamps of a set of photos in Lightroom? A bit of research taught me I would need to either buy or create a Lightroom plug-in. Being both stingy and keen to learn stuff like how to write a Lightroom plug in, I chose: create!

Martin Fowler has a very helpful page for people that want to create their first Lightroom plug-in. This page also has a line that rings true for me.

“As a programmer, I’m always looking for ways to spend several hours programming to save an hour’s work.”

Martin Fowler

I feel seen.

So I followed the basic arc of Martin’s Lightroom/Lua journey, and was able to extract the needed time stamp values.

For the curious among you, it looked something like this:

I was then able to clean it up and import it into Numbers. (And struggle to get Numbers to show a scatter plot where each series has its own X values. Not intuitive. Thank you YellowBox) And I created a graph like this:

I was definitely going to need to stretch it out, in order to be try out different offset values. Once I stretched it, I was able to shift the blue dots left so that the green dots all fell in gaps.

Of course, next time I’m in this situation I will remember to sync my cameras before I start. And as a fallback, if I do end up using un-synched cameras, I will be sure to fix the photo metadata before I sync the camera clocks.

And for any Lua fans out there, here’s my plug-in code.

local LrApplication = import 'LrApplication'
local LrLogger = import 'LrLogger'
local LrTasks = import 'LrTasks'

local myLogger = LrLogger( 'lightroomLogger' )
myLogger:enable( "logfile" )

local function log( message )
  myLogger:trace( message )
end

local catalog = LrApplication.activeCatalog()

local function findCandidates()
	return catalog:findPhotos {
	   searchDesc = {
			 criteria = "captureTime",
			 operation = "thisMonth",
			 value = 2024-08-04,
	   }
	} 
 end

local function showDates()
	LrTasks.startAsyncTask(function()
		local photos = findCandidates()
		for i, v in ipairs(photos) do 
			-- local prop = v:getRawMetadata("fileSize")
			local createTime = v:getRawMetadata("dateTime")
			local colourName = v:getRawMetadata("colorNameForLabel")
			local createTimeOriginal = v:getRawMetadata("dateTimeOriginal")
			local fileName = v:getRawMetadata("path")
			local result = "orig, " .. createTimeOriginal .. ", " .. createTime .. ", " .. colourName .. ", " .. fileName
			log(result)
		end
	end )
end

showDates()

Rovers Vs 2009 Boys

Categories
Software

How to listen for changes in an @AppStorage value

For my flash cards app, I’ve added a few user settings that I’ve discussed in other posts. For example, users can choose whether they want to use the same ask language for all cards (e.g. Always first show words in Japanese). They can also choose whether they want to use かな or Romaji for Japanese. They can also choose a subset of the cards they want to use (eg numbers, months/daysOfTheWeek etc.)

Any time one of these settings gets changed by the user, the app’s LanguageManager singleton needs to update the list of available words that match the settings. (for example, if a given word does not have a French translation and the user specifies fr as their only answer language, that card can’t be shown to the user.)

Strictly speaking, available cards could be recalculated on demand. (ie each time a user switches to a new card) But that would be suboptimal, given the available cards only changes when one of the settings changes. But in the spirit of saving cycles, I feel like finding a way to calculate available cards only when the value changes is a worthy cause.

For a starting point, I have the following code where the UI is setting the UserDefault value.

struct CardPickerView: View {
    
    @AppStorage("cardPile") var cardPile: CardPile = .allRandom
    var body: some View {
        VStack {
            Picker("Card Pile", selection: cardPile) {
                ForEach(CardPile.allCases,
                        id: \.self) { 
                    Text($0.title)
                        .tag($0.rawValue)
                }
            }
        }
    }
}

And the following code where LanguageManager updates availableCards

class LanguagesManager {
    static let shared = LanguagesManager()

    // The UserDefaults values that will impact which cards/words can be used
    @AppStorage("scriptPickers") var scriptPickers:  ScriptPickers = ScriptPickers.defaultDictionary
    @AppStorage("languageChoice") var languageChoice: LanguageChoice = .all
    @AppStorage("cardPile") var cardPile: CardPile = .allRandom

    // All the words
    let words: Words

    // The words that match the criteria
    var availableWords: Words

    func updateAvailableWords() {
        var result = words.filtered(by: cardPile.wordList)
        let askLanguages = languageChoice.askLanguages
        let answerLanguages = languageChoice.answerLanguages
        result = result.matching(askIdentifiers: askLanguages, answerIdentifiers: answerLanguages)
        availableWords = result
    }
}

This code mostly works as expected, however when a user updates scriptPickers or languageChoice or cardPile, availableWords doesn’t get updated. <frownyFace!!> My first thought was to use didSet (like this)

    @AppStorage("cardPile") var cardPile: CardPile = .allRandom {
        didSet { updateAvailableWords() }
    }

Sadly didSet on @AppStorage only works in very specific situations for me. Based on what I saw, it works if:

  1. your code is in a View
  2. you explicitly update the value (eg. self.cardPile = .all)

Your mileage may vary. Bottom line: didSet in LanguagesManager was not getting called for me.

It would have been possible to add calls to updateAvailableWords() in the UI code any time one of the related settings changed. But relying on the UI code to keep LanguagesManager fresh. Just. Felt. Wrong.

I also tried to use combine to subscribe to the @AppStorage values, but was not able to get that working. <anotherFrownyFace>

The solution I ended up using was to create a UserDefaults extension that would create a Binding<> for a specific UserDefaults key value. Anytime this value changes, it will fire a notfication.

extension UserDefaults {
    func cardPileBinding(for defaultsKey: String) -> Binding<CardPile> {
        return Binding {
            let rawValue = self.object(forKey: defaultsKey) as? Int ?? 0
            return CardPile(rawValue: rawValue) ?? .allRandom
        } set: { newValue in
            self.setValue(newValue.rawValue, forKey: defaultsKey)
            NotificationCenter.default.post(name: .userDefaultsChanged, object: defaultsKey)
        }
    }
}

extension Notification.Name {
    static let userDefaultsChanged = Notification.Name(rawValue: "user.defaults.changed"
}

Next, I used these bindings in the UI code where I’d previously been using @AppStorage.

struct CardPickerView: View {
    let cardPileBinding: Binding<CardPile>
    init(userDefaults: UserDefaults = .standard) {
        self.cardPileBinding = userDefaults.cardPileBinding(for: "cardPile")
    }
    var body: some View {
        VStack {
            Picker("Card Pile", selection: cardPileBinding) {
                ForEach(CardPile.allCases,
                        id: \.self) { 
                    Text($0.title)
                        .tag($0.rawValue)
                }
            }
        }
    }
}

Then I updated LanguagesManager to listen for this notification.

class LanguagesManager {
    private var subscriptions = Set<AnyCancellable>()
    init() {
        // a bunch of unrelated init stuff....

        NotificationCenter.default.publisher(for: .userDefaultsChanged)
            .sink(receiveValue: { notification in
                self.updateAvailableWords()
            })
            .store(in: &subscriptions)
    }

And this meets my needs, insofar as the UI code doesn’t need to explicitly fiddle with LanguagesManager state any time a user changes their prefs. I don’t like that the UI code needs to use this special binding to do the updating. I also don’t like that the UserDefaults extension needs to define multiple functions to return the different types of Bindings. If you all know of a way to get around this (generics?) I’m all ears!

I also don’t like that the getters and setters in the Binding values need to translate to and from rawValue. @AppSupport magically handled the translations between rawValue and encodedValue.

And one last problem with this approach, for at least one of my settings UI inplementations, calling the setter in the binding didn’t trigger an update to the UI. (it’s on my todo list to investigate this.) I got around this problem by adding a ‘dummy’ @AppStorage var in the View. Adding this dummy value seems to force the view to redraw when the UserDefault value changes.

    @AppStorage("cardPile") var cardPileDummy: CardPile = .allRandom

Hurricanes Vs South Surrey

HurricanesVsSouthBurnaby

AltitudeWomens

UnityFC

GMedia1

Categories
Software

A Gory Detail For Xcode Test Targets

Somehow, I’ve gone for years without being aware of this setting in the Test Targets used in Xcode projects. Apologies if this has been obvious to everyone on Earth other than me.

A Tangentially related background story, that enabled me to discover this setting

In my flash cards app, I needed logic to figure out which cards could be used for a given user’s ask/answer languages setting. For example, if a user wants to always use Korean as their ask language, and all other languages available as answer languages, then only cards that included Korean and at least one other language could be presented.

Also, bear in mind that for languages that have roman and non-roman script versions, they need to be treated as two separate languages, only one of which is relevant, depending on which the user has selected in Settings.

My first pass at the logic to pick the next card erred on the side of simple, rather than safe. It used the following process:

  1. Select the ask language (randomly if the user’s preference specifies multiple ask languages)
  2. Determine an answer languages list (again shuffle them randomly if there is more than 1 answer language)
  3. Find the list of words matching the ask language and the answer languages (a bug here was returning an empty word list when I specified a single answer language and there were no words yet with that language)
  4. Pick a word from the list
  5. Calculate the intersection of available languages for the word and answer language(s) specified by the user
  6. Return an object containing the wordId, the askLanguage and the array of answerLanguages

As I mention in the list, there is a bug in step 3 that can cause a crash later in the process. (Better would be to say at the get go, something like: ‘I don’t have any words that match your languages choices’

Also, the above has a highly buried dependency on the user’s setting/pref on whether they want to use the roman version or the non-roman version of languages that have their own special script/alphabets.

So (we’re getting VERY close to the topic of this post) I am currently in the process of improving this process by pulling the user setting dependency out (or at least make it injectable when testing.)

The Point

So I wrote a test, and attempted to run it. But when I ran my single test, with its shiny new injected dependency it never ran, because Xcode first insisted on running my main app, which of course was still crashing when attempting to use the old/broken logic for picking a card.

Thanks to a great answer from the ever helpful Quinn, I was able to discover this setting in Xcode.

Once I set it to None, I was able to run my stand alone test, and resume creating my improved card picking logic.