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: [
]
}
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:
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.
.