Custom Theme Example

This example shows how to package a custom theme with a custom font as a service package. Since this is less a matter of implementing complicated logic than making sure the required parts are packaged correctly, the main emphasis in this example is on the bundler configuration and the directory structure.

Caution: Under construction

Since the underlying implementation is still heavily worked on, the API and configurations used in this example are expected to undergo further changes in upcoming releases. Map themes are currently not applied in the SDK runner.

Overview

The goal of this example is to add a new theme to HNOD. The new theme does not include any notably different styling except for a custom, monospaced font. To accomplish this, a new service package is defined that includes a theme that we'll call "Monospaced" and the asset for the custom font.

Package Structure

The top-level package structure should be very familiar by now. Of particular note are:

  • The bundler configuration in hnod.bundler.json
  • The assets folder, which contains the service package's assets
  • The src folder, which contains the TypeScript code (including the camera configuration)
  • The map-theme folder, which contains the map theme assets.

To understand how everything is tied together, a more detailed look at the individual parts is needed.

Camera Configuration

Camera configuration parameters are usually stored inside src/cameraConfiguration.ts file. The file exports a variable of CameraConfiguration type which is later included into the theme definition:

import { CameraConfiguration } from "@here/hnod-sdk";

export const myCameraConfiguration: CameraConfiguration = {
  modes: [
    // List of camera modes, see description below.
  ]
}

Then in NodeBundle.ts:

import { bundleWith } from "@here/hnod-sdk";
import { myCameraConfiguration } from "./cameraConfiguration";

export default bundleWith({
  themes: () => [
    {
      id: "com.example.mytheme",
      name: "My Theme",
      uiThemeId: "com.example.mytheme",
      cameraConfiguration: myCameraConfiguration,
      mapTheme: {
        themeFile: "my-map-theme.scene.json",
        resourceDirectory: "map-theme/my-map-theme/"
      }
    }
  ]
});

Description of camera configuration parameters

Camera configuration consists of a list of camera modes. Each entry describes a specific camera mode, like 2D or 3D. Here are the parameters of the camera mode:

  • name: string - name of the mode that is being described. The only modes that are currently supported are 2D and 3D.

  • cameraModeConfiguration - describes general configuration of this camera mode:

    • fov: number - field of view in degrees that the camera should use for this mode. Must be more than 0º and less than 180º.

    • maneuverScreenPosition: Point - position on the screen where maneuver should be "pinned" in the approaching maneuver mode. Specified in normalized screen-space coordinates, where {x: 0, y: 0} is top left and {x: 1, y: 1} - bottom right of the screen. Due to technical limitations, x value is currently ignored and maneuver point is always centered horizontally (in other words, x is always 0.5).

    • cameraParameters - definitions for camera tilt and zoom per road class:

      • default: TrackingParameters - fallback value for when roadClassMap (see below) doesn't contain definition for the specific road class.

      • roadClassMap - map that defines camera tilt and zoom for the specific functional road classes. Each entry looks like this: <road-functional-class>: TrackingParameters.

    • maneuverRules - an array of camera parameters for approaching maneuver mode:

      • actions: string[] - a list of maneuver actions (see below) for which the rule applies. Empty means all (don't filter by actions).

      • roadClasses: <road-functional-class>[] - a list of road functional classes for which the rule applies. Empty means all (don't filter by functional classes).

      • distanceRange: DistanceRange - minimum and maximum distance at which the rule should be active.

      • maneuverParameters: ManeuverParameters - tilt and maximum allowed zoom of the camera.

Additional definitions

Point, TrackingParameters, and ManeuverParameters are defined like this:

interface Point {
  x: number;
  y: number;
}

interface TrackingParameters {
  tilt: number;
  zoom: number;
}

interface ManeuverParameters {
  tilt: number;
  maxZoom: number;
}

type DistanceRange = [number, number];

The <road-functional-class> must be one of the following:

  • FUNCTIONAL_CLASS_1 - roads allow for high volume, maximum speed traffic movement between and through major metropolitan areas. Access to the road is usually controlled.

  • FUNCTIONAL_CLASS_2 - roads are used to channel traffic to FUNCTIONAL_CLASS_1 roads for travel between and through cities in the shortest amount of time.

  • FUNCTIONAL_CLASS_3 - roads that intersect FUNCTIONAL_CLASS_2 roads and provide a high volume of traffic movement at a lower level of mobility than FUNCTIONAL_CLASS_2 roads.

  • FUNCTIONAL_CLASS_4 - roads that provide for a high volume of traffic movement at moderate speeds between neighbourhoods.

  • FUNCTIONAL_CLASS_5 - roads with volume and traffic movement below the level of any other functional class.

  • FUNCTIONAL_CLASS_UNKNOWN - the functional class of the road is unknown.

List of known maneuver actions: depart, departAirport, arrive, arriveAirport, arriveLeft, arriveRight, leftLoop, leftUTurn, sharpLeftTurn, leftTurn, slightLeftTurn, continue, slightRightTurn, sharpRightTurn, rightUTurn, rightTurn, rightLoop, leftExit, rightExit, leftRamp, rightRamp, leftFork, middleFork, rightFork, leftMerge, rightMerge, trafficCircle, ferry, leftRoundaboutExit1, leftRoundaboutExit2, leftRoundaboutExit3, leftRoundaboutExit4, leftRoundaboutExit5, leftRoundaboutExit6, leftRoundaboutExit7, leftRoundaboutExit8, leftRoundaboutExit9, leftRoundaboutExit10, leftRoundaboutExit11, leftRoundaboutExit12, rightRoundaboutExit1, rightRoundaboutExit2, rightRoundaboutExit3, rightRoundaboutExit4, rightRoundaboutExit5, rightRoundaboutExit6, rightRoundaboutExit7, rightRoundaboutExit8, rightRoundaboutExit9, rightRoundaboutExit10, rightRoundaboutExit11, and rightRoundaboutExit12.

Here'a an example of the camera configuration for 3D camera mode:

const cameraConfiguration: CameraConfiguration = {
  modes: [
    {
      name: "3D",
      cameraModeConfiguration: {
        fov: 60,
        maneuverScreenPosition: { x: 0.5, y: 0.25 }
      },
      cameraParameters: {
        default: { tilt: 50, zoom: 17 },
        roadClassMap: {
          FUNCTIONAL_CLASS_1: { tilt: 60, zoom: 16 },
          FUNCTIONAL_CLASS_2: { tilt: 60, zoom: 16.5 },
          FUNCTIONAL_CLASS_5: { tilt: 45, zoom: 17.5 },
          FUNCTIONAL_CLASS_UNKNOWN: { tilt: 45, zoom: 17.5 }
        }
      },
      maneuverRules: [
        {
          actions: [],
          roadClasses: ["FUNCTIONAL_CLASS_1", "FUNCTIONAL_CLASS_2"],
          distanceRange: [0, 1300],
          maneuverParameters: { tilt: 60, maxZoom: 20 }
        },
        {
          actions: [],
          roadClasses: ["FUNCTIONAL_CLASS_3", "FUNCTIONAL_CLASS_4"],
          distanceRange: [0, 500],
          maneuverParameters: { tilt: 50, maxZoom: 20 }
        },
        {
          actions: [],
          roadClasses: ["FUNCTIONAL_CLASS_5", "FUNCTIONAL_CLASS_UNKNOWN"],
          distanceRange: [0, 300],
          maneuverParameters: { tilt: 45, maxZoom: 20 }
        }
      ]
    }
}

Map Theme

Inside the map-theme folder, you can define your own custom_map_theme folder. Inside your own custom_map_theme folder, hnod_objects, and images are resolved relative to custom_map_theme.scene.json. They all share one parent directory. The Image/icon assets are per theme, and not shared.

custom_map_theme
├── hnod_objects
│   ├── ccp.style
│   ├── marker.style
│   ├── route_incident.style
│   ├── route_line.style
│   └── route_marker.style
├── images
│   ├── incident.png
│   ├── fallbackIcon.svg
|   ...
│   ├── icons.json
|   ...
│   └── zooSmall.svg
├── ...
└── custom_map_theme.scene.json

FallbackIcon

The fallbackIcon can be be defined in custom_map_theme/images/icons.json by defining an icon with the name attribute "fallbackIcon". When the fallbackIcon is defined, it will show the icon on map instead of none when one category doesn't have a defined icon. If an icon for another category would be defined, the explicitly defined icon would be used for that category. The icons sizes are for instruction purposes only; they can be freely defined. The fallbackIcon is an opt-in feature.

{
  "icons": [
    {
      "name": "fallbackIcon",
      "file": "my_chosen_fallback_icon.svg",
      "size": { "width": 99, "height": 120 },
      "labelScaling": 1.5,
      "anchorOffset": { "x": 0, "y": 0.5 },
      "labelOffset": { "x": 0, "y": 0 },
      "refHeight": 1080
    }
  ]
}

Bundler Configuration

The bundler configuration is very similar to the ones from earlier examples. It contains the list of entry points, and some metadata about the package we want to build. One thing is notably different though - under assetsExtensions we now have the following:

{
  ...
  "assetsExtensions": [
     { "type": "woff2", "as": "url" }
  ],
  ...
}

This instructs the bundler to use webpack's url-loader for files with the extension "woff2" (Web Open Font Format 2.0). We need this configuration so that the new font file that we add in this service package is included with the resulting code, and cached for offline use.

Providing the assets

In addition to configuring the bundler to load web fonts with the url-loader, we need to actually provide an asset for the font. To that end, a new file with the monospaced web font that our theme will use has been added under assets/FiraMono-Regular.woff2.

The idea behind assets it to allow the programmer to import such files like normal TypeScript modules. To make this work properly, additional glue code is needed: src/assets.d.ts contains module declarations to make the TypeScript compiler allow imports from such assets.

The Theme

An HNOD theme can configure various aspects of the UI. At a very high-level, themes are just objects composed of well-known types that contain configuration values which are loaded by the HNOD runtime and injected into the UI when a service package is enabled in the HNOD portal. Different aspects of UI-theming correspond to different parts of a theme object with different well-known types.

This example only discusses some of these types in more detail. For the many other values that a theme contains, please refer to the respective API documentation.

The FontTheme

The font-related aspects are configured in an object of type FontTheme.

  • The FontTheme instance of the example theme is defined in src/theme.ts in the export fontTheme.
  • The font theme contains a CSS rule for the normal font family under the property normal. All other font variants (e.g. bold, etc.) are computed relative to this normal font. In our example theme we configure the normal font to prefer a new font-family "firaMono" before falling back to other fonts.
  • To ensure that this font family "firaMono" is actually available, we define a custom font face under the property fontFaces. The custom font face is configured with the fontFamily set to "firaMono" and its src is configured to use the contents of the asset from assets/FiraMono-Regular.woff2 that is imported at the top.

Custom CSS styles

For more advanced theming, the high-level themable properties in the theme object might not be sufficient. To handle such cases, it is possible to drop down to the CSS level and add additional rules. These are exposed in the styles property of the theme object. For a simple example, we could specify a CSS rule set that applies a text transform to some of HNOD's components via their pre-defined BEM-style class names:

export const styles: CssWithTheme = {
  ".route-preferences-button .route-preferences-button__label": {
    textTransform: "uppercase"
  }
};

CssWithTheme supports plugging in CSS styles in a couple of formats:

  • object literals that represent CSS rule sets (see above)
  • raw strings as CSS fragments
  • arrays with a mix of the aforementioned formats
  • functions that return either of the above and accept the scaled theme. This is useful since there are display-specific scaling factors that are applied to all size measurements in the theme. If the CSS styles that should be created must take the scaling into account (or be dynamic in some other way), this is a way to return adapted CSS.

In our example, let us assume we also want to customize the font sizes and weights of some elements according to the to the scaled font size. We can accomplish this using the function syntax to override the default font sizes:

export const styles: CssWithTheme = (theme) => ({
  ".route-preferences-button .route-preferences-button__label": {
    textTransform: "uppercase",
    fontSize: theme.sizeOf.tinyFont
  },
  ".card-footer-button .card-footer-button__label": {
    textTransform: "uppercase"
  },
  ".route-item .route-item__eta-value": {
    fontWeight: "bold"
  },
  ".route-item .route-item__distance-value": {
    fontWeight: "bold"
  }
});

Support for multiple displays

HNOD supports multiple displays, and by default a theme has the same effect on the UI on every display. However, it is possible to create display specific themes. Instead of exporting individual theme objects, a theme can export a default function that takes a displayId parameter and returns a Theme object. The displayId can then be used to define display specific theme values.

export default (displayId: string): Theme => {
  const themeId = "theme-hnod-display-example";
  const themeName = "Display example";

  const styles: CssWithTheme = (theme) => ({
    ".route-preferences-button .route-preferences-button__label": {
      textTransform: "uppercase",
      fontSize: displayId === "layout.main" ? theme.sizeOf.tinyFont : 50 
    }
  });

  return {
    themeName,
    themeId,
    styles
  }
});

The WebRunnableBundle for the UI

Like all service packages, our theme package still needs at least one entry point that exposes the service package's contents to the HNOD runtime environment.

In this case, we are only exposing a strictly UI-related theme. Therefore our only entry point is of type WebRunnableBundle and is exported from src/bundle.ts. Within the bundle we only expose a single theme object that contains our font theme along with a descriptive name which can be presented to the user for selection. Since this example is concerned with providing a custom monospaced font, we set this themeName to "Monospaced".

Build the Example

To build the service package, we can once again use:

yarn build

This processes assets, compiles the TypeScript code and outputs a single ZIP archive that is ready for execution in the HNOD runner and eventual upload to the HNOD portal.

Run the Example

Once the ZIP is built, we can use the runner to try it out locally:

yarn run

When we switch to the settings now, our new theme "Monospaced" is available under "Theme settings". When we select this theme, the font in the UI changes to the custom font we included in the package.

HNOD with the "Monospaced" theme.

results matching ""

    No results matching ""