Argus
Configuration

The app composes; modules provide.

Nothing below the application layer knows that a triangle means air or that cyan means ours. What the map means is declared in the application's QML, and changing it recompiles nothing but that file.

the mechanism
Argus.Symbology

MapSymbol, SymbolRegistry, IdentityRule, adornments

the geometry
Argus.Tactical.Symbols

Triangle, F35, Helicopter, EmitterDishAdornment

the policy
the application

which shape answers for which facts, in which color

1. Color from whose it is

Affiliation to color is declared once and read by both the map and the information panel, so a contact cannot be one color on the map and another in a panel. The three ways of saying not identified all fall to the untagged entry.

Main.qml
AffiliationColors {
    id: affiliationColors

    AffiliationColor { standard: StandardIdentity.Hostile;       color: Theme.identityHostile }
    AffiliationColor { standard: StandardIdentity.Suspect;       color: Theme.identityHostile }
    AffiliationColor { standard: StandardIdentity.Friend;        color: Theme.identityFriend }
    AffiliationColor { standard: StandardIdentity.AssumedFriend; color: Theme.identityFriend }
    AffiliationColor { standard: StandardIdentity.Neutral;       color: Theme.identityNeutral }
    AffiliationColor { color: Theme.identityUnknown }   // no identity: the fallback
}

2. Shape from what it is

A TrackSymbology is a list of IdentityRules read top to bottom. Each names some combination of environment, platform, and specific type off the UCI identity, and the symbol that answers for it. Adornments and animations hang off the symbol and gate on the tags a track carries, such as emitting or stale.

Main.qml — the picture
TrackSymbology {
    id: trackSymbols

    // The state words a symbol may name in a `when`; a typo warns at load.
    vocabulary: TrackTags.all
    colorMap: affiliationColors

    // Rules match in declaration order and the first match wins, so the
    // specific rules go first and the rule stating nothing is the fallback.
    IdentityRule {
        environment: Environment.Air
        symbol: Triangle {
            name: "air"
            animations: [ Pulse { when: TrackTags.stale; period: 2.0 } ]
            EmitterDishAdornment { when: TrackTags.emitting }
            VelocityVectorAdornment {}
        }
    }
    IdentityRule {
        environment: Environment.Ground
        symbol: Square { name: "ground" }
    }
    IdentityRule {
        symbol: Circle { name: "unknown" }
    }
}

A registry is per layer, not per model. Our own platforms draw the same facts in a different palette, with airframe silhouettes where the wire names one:

Main.qml — our own systems
TrackSymbology {
    id: ownSystemSymbols

    vocabulary: TrackTags.all
    colorMap: AffiliationColors {
        AffiliationColor { color: Theme.ownSystem }   // everything here is ours
    }

    // Most specific first: a named airframe gets its silhouette, a helicopter
    // its own, and any other aircraft the generic fast jet.
    IdentityRule {
        environment: Environment.Air
        specific: DemoSpecificType.F35
        symbol: F35 { name: "ownF35"; VelocityVectorAdornment { startOffset: 11 } }
    }
    IdentityRule {
        environment: Environment.Air
        platform: AirPlatform.Helicopter
        symbol: Helicopter { name: "ownHelicopter"; VelocityVectorAdornment {} }
    }
    IdentityRule {
        environment: Environment.Air
        symbol: Fighter { name: "ownAir"; VelocityVectorAdornment { startOffset: 11 } }
    }
    IdentityRule {
        symbol: Circle { name: "ownAny" }
    }
}

Populations with nothing to distinguish use a plain SymbolRegistry with a constant color:

Main.qml — waypoints
// Waypoints are authored rather than classified, so there is nothing to
// match on: one untagged rule and a constant color.
SymbolRegistry {
    id: waypointSymbols

    colorMap: ConstantColor { color: Theme.marker }

    SymbolRule {
        symbol: Hexagon { name: "waypoint"; filled: false }
    }
}

At load, the registry warns about a rule an earlier one shadows, and about a rule naming a tag outside its vocabulary. An item whose identity matches no rule draws nothing and warns once, because a wrong shape would be worse than none.

3. Adding a symbol

A MapSymbol is pure geometry: a list of flattened contours in symbol-local pixels, origin at the anchor, y negative upward so a nose pointing forward is authored toward negative y. The first contour is the outline; later ones are holes. A shape used by one registry can be declared inline; a reusable one becomes a file in Argus.Tactical.Symbols.

// A MapSymbol is shape only: no color, no criteria. Symbol-local pixels,
// origin at the anchor, y negative upward, contours already flattened.
MapSymbol {
    name: "chevron"
    filled: true
    closed: true
    paths: [
        [ Qt.point(0, -7), Qt.point(6, 5), Qt.point(0, 2), Qt.point(-6, 5) ]
    ]
}

Airframe silhouettes are converted from SVG offline: resolve the path commands, flatten curves, subtract the anchor, scale to the authored size, emit points. Abstract shapes are about twelve pixels across, silhouettes about twenty nose to tail; symbolScale multiplies from there.

4. Models and their lifetimes

Models are plain objects the feeds write into. Each names how long a quiet track stays fresh before it is tagged stale, and how long after that it is forgotten.

Main.qml
TrackModel {
    id: trackModel
    staleAfterSeconds: 6          // quiet tracks gain TrackTags.stale
}

TrackModel {
    id: ownSystemModel
    staleAfterSeconds: 5          // five missed 1 Hz reports is a dropout
    expireAfterSeconds: 30        // half a minute of it has left the mission
}

OwnshipModel {
    id: ownshipModel
    staleAfterSeconds: 3          // ownship reports at 5 Hz
    expireAfterSeconds: 30
}

5. The A-GRA link

The session is one declaration. Feeds attach to it and to the model they own, and the map view is handed each model with the symbology that draws it.

Main.qml
// The A-GRA link. Only the connection is declared here; the feeds that
// write the models declare themselves against it.
LaCal {
    id: cal
    url: "ws://127.0.0.1:21402"
    serviceId: "argus-c2"
    active: true
}

OwnSystemsFeed { session: cal; model: ownSystemModel }
OwnshipFeed    { session: cal; model: ownshipModel }

// Synthetic traffic until a feed drives the picture. It only ever touches
// tracks it created, so fed tracks in the same model are left alone.
DemoLoop { model: trackModel; trackCount: 50 }

MapView {
    anchors.fill: parent
    trackModel: trackModel;           trackSymbols: trackSymbols
    ownSystemModel: ownSystemModel;   ownSystemSymbols: ownSystemSymbols
    waypointModel: waypointModel;     waypointSymbols: waypointSymbols
}

Where the data comes from

Map data is prepared offline and compiled into the Argus.Map module as resources. There is no filesystem loading and no tile server; swapping imagery means rebuilding, which is what makes the same binary work airgapped, in a browser, and inside an Android package.