UI des GIS-Browsers vom Kanton Zürich | Read-only mirror of https://github.com/gisktzh/gb3-web_ui — Kanton Zürich. Issues & pull requests at the source.
  • TypeScript 87.4%
  • HTML 6.9%
  • SCSS 5.3%
  • JavaScript 0.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Pascal Thormeier 36438df79b
Merge pull request #707 from gisktzh/feature/GHUB-448-default-basemap-per-map
Feature/ghub 448 default basemap per map
2026-08-25 16:58:47 +02:00
.devcontainer Feature/ghub 9 lokale entwicklungsumgebung (#199) 2025-04-16 16:55:24 +02:00
.docker GB3-574: change nginx logformat 2026-01-06 15:39:35 +01:00
.github/workflows Merge pull request #665 from gisktzh/renovate/actions-checkout-7.x 2026-07-03 17:04:27 +02:00
.husky GHUB-0: Add knip to pre-commit hook. 2026-03-11 12:57:50 +00:00
.readme Add contributors to README 2024-09-30 16:30:45 +02:00
dev Update nodejs 2026-07-03 07:31:09 +00:00
e2e GHUB-408: Add scale to urls of HAR tests 2026-07-13 16:37:00 +00:00
scripts/pipeline GHUB-0: Do not use apk but apt 2026-06-30 14:50:04 +00:00
src Merge pull request #707 from gisktzh/feature/GHUB-448-default-basemap-per-map 2026-08-25 16:58:47 +02:00
.dockerignore
.editorconfig
.gitattributes Prevent git from changing line endings on checkout 2026-06-24 16:35:14 +02:00
.gitignore GB3-1818: Add Playwright and migrate e2e tests. Also do partial refactor to signals. 2026-03-27 17:28:39 +00:00
.gitlab-ci.yml Update docker Docker tag to v29.7.2 (#719) 2026-08-22 07:33:40 +02:00
.lintstagedrc Update lintstagedrc 2025-01-28 14:26:22 +01:00
.nvmrc Update nodejs 2026-07-03 07:31:09 +00:00
.prettierignore
.prettierrc.json
angular.json GB3-1818: Add Playwright and migrate e2e tests. Also do partial refactor to signals. 2026-03-27 17:28:39 +00:00
docker-compose.ktzh.yml GB3-0: Adjust pipeline to have different job chains per env. Also remove twitter config. 2026-05-21 14:36:12 +00:00
Dockerfile Update nodejs 2026-07-03 07:31:09 +00:00
eslint.config.mjs GB3-1818: Fix signal invocations and add ESLint/TS rules. 2026-06-24 11:28:54 +00:00
knip.config.ts GB3-0: Readd removed exports and do not check for unused exports or 2026-06-24 13:21:07 +00:00
LICENSE.txt GB3-1751: Update year in license. 2026-02-18 08:38:59 +00:00
package-lock.json Update playwright monorepo to v1.62.1 (#720) 2026-08-23 03:48:03 +02:00
package.json Update dependency @arcgis/core to v5.1.20 (#716) 2026-08-22 05:00:06 +02:00
playwright.config.ts GB3-1818: Reenable chrome and webkit, also cleanups. 2026-04-17 13:30:21 +00:00
prepare.mjs Modified prepare.js by renaming it to prepare.mjs and upgrading the husky installation mechanic to use ESM import instead of the old CommonJS require. 2024-01-29 09:26:55 +01:00
README.md GHUB-433: Add visual distinction of intranet vs internet. 2026-07-03 09:39:05 +00:00
remove-generated-api-version-references.js Added remove-generated-api-version-references.js which searches for API version references in type and interface names and then simply removes them. 2024-01-15 08:14:58 +00:00
renovate.json GB3-1799: Adjust renovate config to update Node everywhere instead of only nvmrc. 2026-05-19 13:57:55 +00:00
sonar-project.properties GB3-1818: Fix coverage setting of SonarQube. 2026-04-17 14:31:00 +00:00
swagger.yaml Merge pull request #520 from gisktzh/feature/gb3-1822-remove-dev-mode 2026-03-24 16:25:02 +01:00
tsconfig.app.json GB3-0: Adjust pipeline to have different job chains per env. Also remove twitter config. 2026-05-21 14:36:12 +00:00
tsconfig.json GB3-1818: Fix signal invocations and add ESLint/TS rules. 2026-06-24 11:28:54 +00:00
tsconfig.spec.json GB3-0: Adjust pipeline to have different job chains per env. Also remove twitter config. 2026-05-21 14:36:12 +00:00
update-version.js Fixed update-version.js by using the absolute path to the git executable because relative paths are a potential security issue. 2024-01-15 08:16:36 +00:00
vitest.config.ts GB3-1819: Migrate to vitest and remove zone.js 2026-03-16 17:46:46 +00:00

GB3 Frontend

This project was generated with Angular CLI version 14.2.3.

Table of Contents

  1. Node version
  2. Development server
  3. Docker
  4. Naming conventions
  5. Code documentation
  6. Git conventions
  7. Release management
  8. Contributors

Node version

We strive to use the most recent LTS version. Whenever an update is due, make sure you adjust the following:

  • Dockerfile
  • ./dev/Dockerfile.dev
  • .github/workflows/node*.yml (Warning! There are cases where the pipeline does not yet have the newest node version; in that case, leave it as before and ignore the pipeline warnings)
  • .nvmrc
  • package.json, update the @typed/node package to the matching version; run npm install afterwards to freshly generate the package-lock.json
  • renovate.json update both "matchManagers": ["dockerfile"] and "matchManagers": ["npm"] to the matching version

You should point it towards the latest minor update (e.g. 20.x), such that we can control potentially larger updates.

If you're using nvm on a Unix-based environment, you can conveniently use nvm use in the root directory and it will automatically set the node version to the correct one.

Development server

Run ng serve for a dev server. Navigate to http://localhost:4200/. The application will automatically reload if you change any of the source files.

Code scaffolding

Run ng generate component component-name to generate a new component. You can also use ng generate directive|pipe|service|class|guard|interface|enum|module.

Build

Run ng build to build the project. The build artifacts will be stored in the dist/browser/ directory.

Running unit tests

Run ng test to execute the unit tests via Karma.

Warning

Starting with @arcgis/core 4.32, memory demands have increased significantly due to more types, which requires more work for checking types. As such, karma may run into memory overflows. If this happens, you can increase the memory limit for the node process by setting the NODE_OPTIONS environment variable to --max_old_space_size=80192 (or any other value that suits your needs).

Running end-to-end tests

Installing browsers

To install all necessary browsers, execute:

npx playwright install --with-deps

This will install all dependencies and browsers necessary.

Running the tests

ng e2e

or

npm run e2e

Running tests with HAR mocks

HAR stands for Http ARchive. Playwright is able to use HAR files to play back API responses and make tests more deterministic while allowing for adaptions to test data without having to deploy any environment first.

HAR mocks get written per test file. To update the current ones or create new ones, we need to specify a username and password for any tests that need a logged in user. To specify user and password and tell Playwright to write new HAR files, we execute ng e2e with three environment variables:

WRITE_HAR=1 TEST_EIAM_USERNAME=[some user] TEST_EIAM_PASSWORD=[some password] ng e2e

Keep in mind that you need to replace [some user] and [some password] with a valid test user. Any credentials passed here are automatically redacted from the HAR files afterwards. Additionally, we use a mocked login page when HAR files are used, since we can't 100% replicate the backend flow.

Debugging

To enable "debug mode", do the following steps in order:

  1. Go to playwright.config.ts and comment in the debugging part (i.e. everything below // DEBUGGING)
  2. Execute PWDEBUG=1 ng e2e

This will now start one window per specified .spec.ts file. An additional window is Playwrights own debugger window. From there, you can follow the official docs. If you want to execute a single specific test, you can adjust pass the --files parameter.

If you also want to capture browser console out, you can use the CAPTURE_CONSOLE env var: CAPTURE_CONSOLE=1 ng e2e.

Further help

To get more help on the Angular CLI use ng help or go check out the Angular CLI Overview and Command Reference page.

Docker

Building the image

Before the first start create volume for log files. It has to be done only once

docker volume create nginx-logs

The docker image has to be built for each environment separately, since we cannot use runtime environment configurations.

In order to build the docker image use the following command (adjust tag as needed):

docker build --no-cache --build-arg TARGET_ENVIRONMENT={target_environment} -t gb3-frontend:latest .
  • gb3-frontend is the name of the image
  • latest is the tag used to mark the version of this image
  • target_environment is the target build environment, which is one of the following:
    • local: localhost development
    • local-gb2: localhost development with locally deployed GB2 backend
    • dev-ebp: production deployment for EBP environment
    • staging: production deployment for KTZH staging environment
    • staging-using-productive-gb2-backend: production deployment for KTZH staging environment which uses the productive GB2 backend infrastructure.
    • uat: production deployment for KTZH UAT environment
    • production: default if this variable is missing; production deployment for KTZH production (internet & intranet) environment
  • APP_VERSION: see below (optional)
  • APP_RELEASE: see below (optional)

The target_environment is used to create environment specific build outputs so as to not divulge sensitive information such as internal domains. This is mainly reflected in the runtime configuration mechanism described below.

Overriding app version and release number

During build, the Dockerfile will run npm run update-version. Per default, it tries to extract the last git commit hash as app version number and the last tag as release number. This only works if the build command is run within the context of a repository-checkout; if it fails, it will display UNKNOWN_VERSION.

If you're building the image outside of a repository context (i.e. within a pipeline), you can specify the version and release number explicitly using the following build-args:

  • APP_VERSION: The version number, usually the last git commit hash in --short form
  • APP_RELEASE: The release number, usually in the form of "Release-xx"

An example command would look like this:

docker build --no-cache --build-arg APP_VERSION=MyCustomAppVersion --build-arg APP_RELEASE=MyCustomRelease -t gb3-frontend:latest .

Irrespective of how the versions are extracted, they overwrite the /src/version.ts file, which is in turn exposed via Angular's environment configs.

Run the image

This image exposes port 8080 and can be run like this:

docker run -p 80:8080 -v nginx-logs:/var/log/nginx  --name gb3-frontend gb3-frontend:latest
  • 80:8080 maps the internal port 8080 to the external port 80; the later can be chosen freely
  • gb3-frontend is the name of the image
  • latest is the version tag for this image
  • -v nginxlogs:/var/log/nginx volume folder with log files for filebeat

Naming conventions

Generally, we are orientating ourselves at the default Typescript naming conventions.

WIP - add more naming conventions :)

Branchname and commit message

Whenever possible, a Jira ticket should be referenced in both branchname and commit message:

  • Branches: [feature|hotfix|bugfix]/[gb3|ghub]-[xxx]-[name-of-branch], where xxx refers to a Jira ticket and the name-of-branch is a short summary of the feature/hotfix/bugfix.`
  • Commits: [GB3|GHUB]-[xxx]: Your commit message, , where xxx refers to a Jira ticket

Our githooks check for both the branch name and the commit message. They will output a warning and reject the commmit if they don't match. In case you might want to deviate from these rules having no Jira ticket, use answer-to-life Jira ticket GHUB-42. ES Lint is enabled in the precommit hook. This means that every commit will be checked for linting errors. If there are any, the commit will be rejected. Either fix the error (unused variable or import, etc.) or disable the rule for the specific line or file (any in .spec.ts files). In case of the latter, consider adding a comment explaining why the rule was disabled. Example for such a case:

// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Allow "any" in test file
const someVariable = {} as any;

The rules are defined in the .eslintrc.config.mjs file.

Code documentation

  1. The ActiveMapItem class
  2. Spatial Reference System(s)
  3. State
  4. Runtime configurations
  5. (S)CSS structure
  6. Custom icons
  7. Transformation from GB2 backend API to GB3 interfaces
  8. Error handling
  9. Application Initialization based on share link
  10. Adding new NPM packages
  11. Feature flags
  12. Handling of date objects
  13. Injection tokens

The ActiveMapItem class

The heart of the application is the ActiveMapItem class. All data the user can add to the map (and reorder, toggle visibility, interact with in terms of settings, etc.) is an instance of the abstract ActiveMapItem class. All ActiveMapItems are available in the ActiveMapItemState. In order for the map to correctly render the state, the MapService implementation has to be synchronized with the state: If e.g. an item is added, the MapService has to handle this accordingly: Create a new framework-dependent instance of the layer, add it to the map, handle ordering, etc.

Usage

The ActiveMapItem is an abstract class; the actual implementation is delegated to subclasses within the implementations subfolder, representing different types of layers that can be added to a map, such as Gb2WmsActiveMapItem. Since configuration for these layers differs, the ActiveMapItem has a property settings which is a discriminated union type ActiveMapItemSettings, holding all layer settings. This allows for a flexible combination of ActiveMapItems and their settings.

As a variation of the visitor pattern, the ActiveMapItem also has an abstract method addToMap which the subclasses need to imlement - this method is responsible for adding a given instance of ActiveMapItem to the map by using the appropriate method on the AddToMapVisitor interface.

In order to avoid the Array.filter(m => m instanceof x).map(m => m as x) pattern, the isActiveMapItemOfType typeguard can be used: Array.filter(isActiveMapItemOfType(x)).

Selectors

To get a list of all currently active map items, the selectItems selector from the class active-map-item.selector.ts can be used. This selector returns all non-temporary ActiveMapItems. If you want to get all active map items including the temporary items, you can use the selectAllItems selector instead.

Spatial Reference System(s)

Because we're using different datasources, we cannot always determin what SRS our GeoJSON objects have. In order to specify the SRS of a given GeoJSON object, use the wrapper classes defined in src/app/shared/interfaces/geojson-types-with-srs.interface.ts which also specify the SRS.

All supported SRS in our app are defined as SupportedSrs type.

Using these helper interfaces and types allows us to properly leverage Esri's built-in transformation services without relying on the implicit conversion of coordinates. As such, whenever possible, we should not rely on the geojson package itself, but rather on its wrapper classes.

State

All application-wide state is handled by NGRX.

TODO: Explain our state in detail :)

Mutating nested state

As of now, we still have (deeply) nested state and for as long as we do not normalize our state, this will pose some challenges, namely that mutating the state in reducers requires a deep copy of the current state object. Luckily, there's a nifty package called immer that helps working with mutable objects. All it does is leverage Proxy objects to create "clever" deep copies. Inside its lifecycle, you can mutate the object as if it were mutable, yet it becomes immutable once it is returned. It also is more performant than e.g. structuredClone because it keeps track of what has changed and only changes (deeply) nested objects if they actually did change.

Apart from performance considerations (which could be further optimized by restructuring our state and/or our reducers), the main reason for introducting immer was that our typings broke with rxjs > 7.8.x in that using structuredClone broke type recognition in Angular's compiler, leading to an inconsistent state between IDE and tooling. So it was either using a proper library (as recommended by ngrx (!)) or adding xxx as xyz typecasts after each structuredClone.

Example

Let's take our ActiveMapItemState as an example, which is the state that is most deeply nested:

export interface ActiveMapItemState {
  activeMapItems: ActiveMapItem[];
}

A simple reducer for updating the visibility on a given ActiveMapItem would look like this:

on(ActiveMapItemActions.setVisibility, (state, {visible, activeMapItem}): ActiveMapItemState => {
  const activeMapItems = state.activeMapItems.map((mapItem) => {
    if (mapItem.id === activeMapItem.id) {
      const newActiveMapItem = structuredClone(mapItem);
      newActiveMapItem.visible = visible;
      return newActiveMapItem;
    }
    return mapItem;
  });
  return {...state, activeMapItems: [...activeMapItems]};
});

Not only does it copy the whole object, it also requires a verbose Array.map operation. With immer, we can inject its produce function directly in the on() parameters, like so:

on(
  ActiveMapItemActions.setVisibility,
  produce((draft, {visible, activeMapItem}) => {
    draft.activeMapItems.forEach((mapItem) => {
      if (mapItem.id === activeMapItem.id) {
        mapItem.visible = visible;
      }
    });
  }),
);

This yields the same result as the first example, but it is much more readable and more in a functional programming style. All it does is creating a proxy object of our state (clarified by using draft and not state), which can then be directly modified using Array.forEach - and then, the result is returned automatically.

Using immer with ES6 classes

The above workflow works for all basic Object types as well as interfaces. As soon as ES6 classes are involved, they need to be marked as such. For convenience, we have a IsImmerable interface available that encapsulates this behaviour. Note that if you fail to do so, you will get Cannot assign to read only property xxxx of object errors, because these class instances are not immerized.

Conventions

Some conventions exist and should be adhered to when dealing with immutable state mutations:

  • Always use the immer approach - do not use things like lodash or structuredClone.
  • In reducers, always try to add the produce call at the top-most level to avoid nasty sideeffects.
  • Most likely, you can add the call at the on parameter level as in the example above.
    • If so, name the state variable draft to signal to the user that this is mutable.
  • Only use immer when it is actually needed - if you don't modify deeply nested states, you're most likely not going to need it.

Runtime configurations

The app supports multiple environments with different endpoints. Because the production deployment has different endpoints depending on whether it is access via internet or intranet, these URLs need to be added during runtime, so Angular's environment files do not work.

As a workaround, the ConfigService can be used. This service will check the current hostname and return the given API configurations. The replacements are done (similar to the environment configurations) as part of the angular.json build file replacements.

The configurations are found in src/app/shared/configs/runtime.config.ts and configured via their environment replacement files.

API config types

Within the runtime configurations there are API configurations used to configure any endpoint URLs. There are currently three types of API runtime configurations:

  • ApiBaseUrlConfig
    The most basic one. Only contains one property: the baseUrl.
  • MockedApiBaseUrlConfig
    A relict from older times where the APIs weren't as stable and we regularly needed mock data. This is a child class from ApiBaseUrlConfig and has one more flag useMockData to indicate whether to use mock data instead of the real service.
  • VersionedApiBaseUrlConfig
    Some of the backend APIs have changing versions to separate breaking changes. This child class from ApiBaseUrlConfig contains a property version where this comes into play. Each environment can use its own versioned API endpoints.

Available URL configurations

Stagename Subdomain Verwendung GB2 Backend WMS Backend Geolion Bemerkung
DEV dev.geo.zh.ch EBP
PROD geo.zh.ch öffentlich maps.zh.ch wms.zh.ch geolion.zh.ch
PROD geo.ktzh.ch Verwaltung web.maps.zh.ch web.wms.zh.ch geolion.ktzh.ch
UAT uat.geo.ktzh.ch Verwaltungsinterne Tests uatmaps.kt.ktzh.ch uatwms.kt.ktzh.ch uatgeolion.kt.ktzh.ch
STAGING staging.geo.ktzh.ch Produktionsvorbereitung testmaps.kt.ktzh.ch testwms.kt.ktzh.ch testgeolion.kt.ktzh.ch

(S)CSS structure

BEM - structured CSS

We are using BEM to structure our (S)CSS: https://getbem.com/introduction/

Basically there are three important elements to keep track of:

  • blocks
    Standalone entity that is meaningful on its own.
    Example: active-map-item-header
    How to use: active-map-item-header (no change)
  • Element
    A part of a block that has no standalone meaning and is semantically tied to its block.
    Example: header-title
    How to use: active-map-item-header__title (connect to a block using two underscores)
  • Modifier
    A flag on a block or element. Use them to change appearance or behavior.
    Example: disabled
    How to use: active-map-item-header--disabled (connect to a block or element using two dashes)

Global functions / variables / mixins / overrides

Each component is responsible for its own styling. However, to prevent too much code duplications we have some global helper files in our \styles folder:

  • functions/... contains some helper functions to calculate e.g. the RGBA value of a hex value.
  • mixins/... contains mixin files divided into categories used to style specific sections of the application. These are the styles that can be shared between different components.
  • overrides/... contains a couple of style files used to globally override certain elements. Use with caution.
  • variables/_ktzh-design-variables.scss contains all important variables used within the GB3 application. Most notable the color palettes that are used everywhere. Try to avoid hard-coded color values inside some local SCSS file.
  • variables/_z-index-variables.scss contains all z-indices ordered by the highest value first. This is used to keep track of which element should be on top of which element in one place.
  • _headings.scss contains all heading styles used in the application. This is used to keep track of all heading styles in one place. They are based on the design system of the Canton of Zurich (see Figma).

To use those global styles within a local SCSS file use the following syntax (or part of it):

@use 'functions/helper.function' as functions;
@use 'mixins/helpers.mixin' as mixins;
@use 'mixins/material.mixin' as mat-mixins;
@use 'variables/ktzh-design-variables' as ktzh-variables;
@use 'variables/z-index-variables' as z-index-variables;

Example of a potential usage:

.button {
  background-color: functions.get-color-from-palette('secondary-accent');
}

The get-color-from-palette function optionally accepts a hue value. A list of all available palettes:

  • 'primary'
  • 'primary-light'
  • 'secondary-accent'
  • 'accent'
  • 'warn'

The setup itself distinguishes between intranet and internet and switches the colours automatically.

Custom icons

All custom icons are handled via the IconsService and the iconsConfig. Add the identifier and the path to the URL ( relative or absolute) and the service adds the icons. Use them as follows:


<mat-icon svgIcon="icon_id_from_config"></mat-icon>

In order to have the SVG adjust itself to the color (e.g. disabled state), replace all fill="color" occurrences in the SVG which should be assigned the font color with fill="currentColor". In some cases, the color might also be within stroke or other attributes.

Transformation from GB2 backend API to GB3 interfaces

To separate the GB2 API interfaces from the internal ones we use transformation methods inside the corresponding GB3-services. Usually it's used to adjust some minor issues like naming (gb2_url => gb2Url). There are a few exceptions where more logic is used to transform values:

Topics endpoint

There are many mayor changes during the transformation of the topics.json from the GB2 API. First of all the naming is different:

GB2 API interface name GB3 interface name Example
category topic Luft und Klima
topic map Lichtemissionen
layer layer August 2018

Another noticeable change is the order of layers. WMS 1.3 describes the order as follows:

A WMS shall render the requested layers by drawing the leftmost in the list bottommost, the next one over that, and so on.

Meaning that the layer with the lowest index has the lowest visibility. However, in GB3 the order is inverted to that as the item with the lowest index has the highest visibility. Therefore, the order of the GB2 API layers get inverted to tackle that problem.

Error handling

The global error handler is located in the error-handling module. All errors are caught by Angular and delegated to this handler which then handles the errors according to their type. Additionally, while in develop mode, the errors are logged to the console.

The application itself defines errors that extend the native Error object, which allows for easier handling and runtime error checks. These abstract errors are defined within app/shared/errors/abstract.errors.ts. The abstract base class of all custom errors is Gb3RuntimeError. It defines an (optional) property originalError of unknown type which can be used to wrap any caught error. The error handler will then check whether this property is set and log the original message as well.

All errors should extend from the following abstract classes, extending Gb3RuntimeError. They have different behaviour in the error handler:

  • FatalError: This error will raise an error that prevents the current screen from being used by redirecting to our fatal error page.
  • RecoverableError: This error will pop an error notification, but will not prevent the app from being used.
  • SilentError: This error will do nothing. Useful for errors that should not be communicated to the user.

Of course, all other errors that might be thrown in the code and that are not caught (e.g. simple throw new Error('Fail!')) will be handled as well; and currently, they are treated as FatalError because we cannot reliably determin whether an error is critical or not.

Implementing custom error classes

Implementing a custom error class is as simple as extending from one of the mentioned base classes. In most cases, you should add a public override message: string = 'Your Error Message' to the class, and you can, of course, add custom logic.

If no constructor is specified, the constructor of Gb3RuntimeError is taken that can be supplied (optionally) with the actually thrown error.

Handling errors

In general, throwing an error is straight-forward: Just throw it.

In practice, there are situations where this is not as simple: In situations where we have an API call within an effect and also use a loadingState, we cannot directly use catchError in the service API call's pipe chain, because this would only show the error message without updating the loading state. For these cases, you should add a dedicated setError action which sets the loading state through the reducer, and then have another effect that listens to this action and then raises the appropriate error. In order to also have the originally thrown error, the helper errorProps() can be used as ActionProp so you can pass along the original error for usage within the effect. For examples of this, see e.g. the LegendEffect.

Importantly, if you throw exceptions within the constructor of a service, make sure to inject the ErrorHandler interface and throw it explicitly using the handleError method. Otherwise, depending on the order of Angular's DI, the error handler might not yet be registered and throw the exception outside of the Angular error handler.

Map page Initialization

When the /maps page is opened without any further parameters, the initial center and scale of the map is calculated based on the viewport. This is required because all the UI-Elements are placed above the map page and can potentially cover parts of the Canton of Zurich. Thus, the map cannot be centered on the entire page but its center needs to calculated dynamically. For this, a number of constant are defined in the map-page.constants.ts file. This includes the bounding box of the Canton of Zurich and the paddings to make sure nothing is covered by the UI-Elements. There are different constants for regular and tablet view and mobile view. These constants describe the number of pixels the center needs to be shifted in each direction. These numbers are derived from the UI-Elements and need to be updated whenever any of these elements are changed. The following list explains how each value is derived and on which value it is based:

  • For Regular and Tablet View:

    • Left (474px):
      • $map-overlay-width (450px) & $map-overlay-width-adjustment(12px) (_map-layout-variables.scss)
      • standard padding (12px, not globally defined)
    • Right (180px):
      • .basemap-selector__active-class (width: 96px, basemap-selector.component.scss)
      • --mdc-icon-button-state-layer-size (48px, Angular Material)
      • .map-controls-class (gap: 12px, map-controls.component.scss)
      • .map-container__map-controls-class (right: 12px, map-page.component.scss)
      • standard padding (12px, not globally defined)
    • Top (88px):
      • Height of the search-bar (64px/100%, search-bar.component.scss)
      • .map-container__search-window-class (top: 12px, map-page.component.scss)
      • standard padding (12px, not globally defined)
    • Bottom (88px):
      • .coordinate-scale-inputs-class (48px: height: 42px + padding 3px (top and bottom), coordinate-scale-inputs.component.scss)
      • .map-controls__inputs-class (gap: 12px, map-controls.component.scss)
      • .map-container__map-controls-class (bottom: 28px, map-page.component.scss)
  • For Mobile View:

    • Left (12px):
      • standard padding (12px, not globally defined)
    • Right (12px):
      • standard padding (12px, not globally defined)
    • Top (84px):
      • @mixin tool-button-mobile (60px _helpers.mixin.scss)
      • .map-container__map-tools--mobile-class (top: 12px, map-page.component.scss)
      • standard padding (12px, not globally defined)
    • Bottom (100px):
      • search-window__searchbar--mobile-class (height: 60px, search-window.component.scss)
      • .map-container__search-window--mobile-class (bottom: 28px, map-page.component.scss)
      • standard padding (12px, not globally defined)

Additionally, there is the NAV_BAR_HEIGHT-Constant in the map.constant.ts file (72px). This needs to be changed if $navbar-height is changed in the _map-layout-variables.scss file.

Loading and initializing of the application based on a previously shared link ID is completely done within the share-link.state. There is the whole initializeApplication and validation part where the application gets initialized. This part is basically a big state machine used to control the initialization flow. It's taking care of all potential side effects like invalid share link item contents or topics that are not getting loaded.

The basic flow based on actions and effects goes like this:

                                     ┌─────────────────────────────────────────────────┐
                                     │ ShareLinkActions.initializeApplicationBasedOnId │
                                     └─────────────────────┬───────────────────────────┘
                    waitForAuthenticationStatusToBeLoaded$ │
                                     ┌─────────────────────▼──────────────────────────────────┐
                                     │ ShareLinkActions.completedAuthenticationInitialization │
                                     └───────┬─────────────┬─────────────────┬────────────────┘
initializeApplicationByLoadingShareLinkItem$ │             │                 │ initializeApplicationByLoadingTopics$
                             ┌───────────────▼───────────┐ │ ┌───────────────▼──────────────────────┐
                             │ ShareLinkActions.loadItem │ │ │ LayerCatalogActions.loadLayerCatalog │
                             └───────────────┬───────────┘ │ └───────────────┬──────────────────────┘
                                             └───────────► │◄────────────────┘
               initializeApplicationByVerifyingSharedItem$ │
                                          ┌────────────────▼──────────────┐
                                          │ ShareLinkActions.validateItem │
                                          └────────────────┬──────────────┘
                                    validateShareLinkItem$ │
                                       ┌───────────────────▼─────────────────┐
                                       │ ShareLinkActions.completeValidation │
                                       └──────┬────────────┬──────────┬──────┘
                 setMapConfigAfterValidation$ │            │          │ setActiveMapItemsAfterValidation$
                  ┌───────────────────────────▼──────────┐ │ ┌────────▼────────────────────────────────┐
                  │ MapConfigActions.setInitialMapConfig │ │ │ ActiveMapItemActions.addInitialMapItems │
                  └──────────────────────────────────────┘ │ └────────┬────────────────────────────────┘
                                                           │ ◄────────┘
                                   completeInitialization$ │
                                 ┌─────────────────────────▼──────────────────────────┐
                                 │ ShareLinkActions.completeApplicationInitialization │
                                 └────────────────────────────────────────────────────┘

Note that error actions/effects are not visible on this diagram

Adding new NPM packages

Usually there are two ways to add new NPM packages. Either calling

  • npm install <package> --save
    To install the package and add it to the package.json (and package-lock.json) in the section dependencies.
  • npm install <package> --save-dev
    To install the package and add it to the package.json (and package-lock.json) in the section devDependencies.

The later (devDependencies) is usually reserved for all dependencies that are not necessary to run the code in a productive environment (e.g. a unit test framework like Jasmine). And usually also stuff like @angular/compiler or @types/.... However, this code gets build inside a docker container using the command npm ci --ignore-scripts --omit=dev. --omit=dev ignores all packages in the devDependencies section. Afterward, a build is triggered. This build of course needs build tools like @angular/compiler which are not available if they're not located inside the dependencies section.

This has the consequence that a lot of packages are located within the dependencies section that would usually be in the devDepencies section.

To test if a new package has to be added to the dependencies or the devDependencies section it's easy to test by running the following commands:

npm ci --ignore-scripts --omit=dev
npm run build-production

Feature flags

Feature flags can be used to toggle features throughout the application. They work as follows:

  • Available featureflags are defined in the FeatureFlags interface.
  • Default values for all features flags need to be defined in the feature-flags.config.ts file.
  • Each runtime configuration might specify overrides for the default values; they are then injected via the ConfigService.
  • The FeatureFlagsService and its getFeatureFlag method is used to access the feature flags.
  • For convenience, the FeatureFlagDirective can be used to toggle elements based on a feature flag.

Handling of date objects

Currently, we are using dayjs to handle date objects. In order to have a high degree of abstraction and to be able to easily replace the library (i.e. using native Javascript features like Intl), all date handlings are done via the TimeService interface, which is implemented as e.g. the DayjsService. Currently, the actual implementation is injected via the timeServiceFactory; and as a convenience, this is also added in test.ts so it does not have to be provided for each and every test.

Injection tokens

Starting with Angular 20, using inject() is preferred over constructor-based injection. This also simplifies the management of abstract classes, since super calls are no longer necessary, because the injection can be fully defined in the parent (abstract) class.

However, moving injection to the parents had the important consequence that tokens can lead to circular dependencies because all tokens were defined in app.module.ts. This happens because a concrete implementation inherits from an abstract representation, which loaded the token from app.module.ts - because we're now having an early instantation, this led to a circular dependency, because the token itself triggers an app module load which in turn triggers the service instantiation again.

The solution was to move all tokens to a dedicated app.tokens.ts file, where they are loaded by both the app module as well as the abstract classes, thus removing the circular dependency.

Git conventions

Branching strategy

Our repository is mainly using the standard Git flow branching model.

Git flow branching model

There are the following branches:

  • main
    The production branch. Every commit has to be stable and tested as it is used as released code. Therefore, every commit that was released is marked by a tag release-XXX where XXX is the release number. It's entirely possible to have multiple release tags on the same commit if this repository wasn't updated since the last releases.
  • develop
    The main development branch. Every commit has to be stable as it will be deployed automatically to the dev-environment server.
  • feature/* , bugfix/*
    Individual feature/bugfix branches. They don't have to be stable as they are connected to one person working on it. They are based on the develop branch and finally get merged into that branch again.
    See also naming conventions for branch naming and commit message format: Branchname and commit message
  • hotfix/*
    This is reserved to fix bugs that occur in a productive environment and need to be fixed ASAP. They're based on the main branch (and not the develop) because it is entirely possible that there are already new features on the develop branch that should not be released. As soon as this branch is finished it needs to be merged back to main (via PR). After that it's very important to create a second PR to merge this branch into the develop branch as well.

Naming conventions

See Branchname and commit message

Release management

This process describes how to tag a release so the team at ARE-GEO-GIS can release the correct version of the application. A more detailed internal version can be found here.

  • Create a new PR called Release from develop into main for all repos that have had changes since the last release.
  • After merging, create a new tag for the release. This can be done by clicking on Releases on the overview page on GitHub and then clicking on Draft a new release.
  • Under Choose a tag create a new tag on publish with the format Release-XX where XX is the release number. This number should be incremented by 1 from the last release.
  • Set the target to main
  • Press Generate release notes to automatically generate the release notes based on the PRs that have been merged since the last release. Clean up if needed.
  • Click on Publish release to create the tag, set it as the latest release and publish the release notes.
  • In Teams, create a new announcement with the release notes and mention which tag is relevant for which repo.

Contributors

The project was developed for the Amt für Raumentwicklung - Abteilung Geoinformation of the Canton of Zurich.

It has been initially developed by EBP Schweiz AG as a closed-source project and has been made open-source in 2024 after the first production release. EBP is still actively contributing and maintaining the project.

Individual contributors

The following people have contributed to this project: