Skip to main content

Creating a custom component

In this tutorial, you will learn how to extend Geogirafe by creating a custom component to draw, modify and delete map markers and their linked attributes. The main learning objectives are:

  • Understanding how Geogirafe components are structured
  • Understanding how Geogirafe components communicate and interact via the state manager (for example how to pass data from the map to a side panel and vice-versa)
  • Getting familiar with the uhtml (micro µ html) templating syntax
  • Getting familiar with the OpenLayers mapping library

If you are just starting with Geogirafe, you may want to check out the Architecture and Client setup pages before continuing.

The tutorial is divided into the following steps:

  1. Setting up the project
  2. Adding a button to the lateral menu bar
  3. Creating the component’s initial structure
  4. Adding markers on the map
  5. Adding select, draw and modify interactions to the map markers
  6. Using the state manager
  7. Creating a subcomponent in the side panel to display a list of chips
  8. Creating a subcomponent in the side panel to display and modify the selected marker's details

The complete code of this tutorial is available here:

Here’s a preview of what you will build:

Hello world component preview

Your custom component will be made of several parts:

  • A button in the lateral menu bar to show/hide your custom hello world component.
  • A side panel with the following elements:
    • Control buttons (a button to toggle the marker draw/move mode and a button to delete all markers)
    • A details box which displays the ID, position and colour of the currently selected marker
    • A list of chips on which you can click to select a marker
  • Map markers which can be selected and moved.

Setting up the project

Clone the Geogiraffe repository (or fork it) locally:

git clone https://gitlab.com/geogirafe/gg-viewer.git

Build the application:

npm install

In this tutorial, we will use the preconfigured SITN demo configuration, you may however create your own configuration or use one of the other preconfigured demos. To use the SITN demo configuration:

  • On Linux, run npm run configure-demo sitn
  • On Windows, run npm run configure-demo-win sitn
warning

Make sure you work on a local drive on Windows, npm will fail on network drives.

Then, start the development server:

npm start

Adding a button to the lateral menu bar

At this step, you will add a new “Hello” button to the lateral menu bar as illustrated below:

Hello World lateral menu button

The "Hello" button will allow users to show or hide the "Hello World" custom component that you will create.

First, add an entry to the graphical interface’s state definition in the state manager:

  1. Open src/tools/state/state.ts

  2. At the beginning of the file, you will see a type definition called GraphicalInterface which has a list of properties (helpVisible, drawingPanel, etc) to control the status and visibility of elements in the graphical interface. Add a boolean property called helloPanelVisible:

    type GraphicalInterface = {
    helpVisible: boolean;
    drawingPanelVisible: boolean;
    printPanelVisible: boolean;
    crossSectionPanelVisible: boolean;
    helloPanelVisible: boolean;
    selectionComponentVisible: boolean;
    selectionComponent: string;
    aboutVisible: boolean;
    shareVisible: boolean;
    darkMapMode: boolean;
    darkFrontendMode: boolean;
    };
  3. Further down in the code, in the State class, locate the interface property, add an entry called helloPanelVisible and set its value to false.

    // Interface configuration (visible panels, ...)
    interface: GraphicalInterface = {
    helpVisible: false,
    drawingPanelVisible: false,
    printPanelVisible: false,
    crossSectionPanelVisible: false,
    helloPanelVisible: false,
    selectionComponentVisible: false,
    selectionComponent: "",
    aboutVisible: false,
    shareVisible: false,
    darkMapMode: false,
    darkFrontendMode: false,
    };

Then, modify index.html (at the project’s root) to add the “hello” button to the lateral menu bar:

  1. Copy the helloworld.svg file to the project's src/assets/icons/ folder

  2. Open index.html in the root directory

  3. In index.html, add the following <button> element in the buttonbar div:

    <div id="buttonbar">
    <button
    slot="menu-content"
    class="gg-icon-button gg-big"
    onclick="document.geogirafe.state.interface.drawingPanelVisible = !document.geogirafe.state.interface.drawingPanelVisible"
    >
    <img alt="help-icon" src="icons/draw.svg" />
    </button>
    <button
    slot="menu-content"
    class="gg-icon-button gg-big"
    onclick="document.geogirafe.state.interface.printPanelVisible = !document.geogirafe.state.interface.printPanelVisible"
    >
    <img alt="help-icon" src="icons/print.svg" />
    </button>
    <button
    slot="menu-content"
    class="gg-icon-button gg-big"
    onclick="document.geogirafe.state.interface.crossSectionPanelVisible = !document.geogirafe.state.interface.crossSectionPanelVisible"
    >
    <img alt="cross-section-icon" src="icons/cross-section.svg" />
    </button>
    <button
    class="gg-icon-button gg-big"
    onclick="document.geogirafe.state.interface.shareVisible = true"
    >
    <img alt="help-icon" src="icons/share.svg" />
    </button>
    <button
    slot="menu-content"
    class="gg-icon-button gg-big"
    onclick="document.geogirafe.state.interface.helloPanelVisible = !document.geogirafe.state.interface.helloPanelVisible "
    >
    <img alt="helloworld-icon" src="icons/helloworld.svg" />
    </button>
    <girafe-layout></girafe-layout>
    <girafe-proj-select></girafe-proj-select>
    </div>
  4. Make sure the development server is running (if it is not, launch it with npm start in the terminal), open your web browser at the url indicated in the terminal (by default http://localhost:8080/). In the lateral button bar, you should see the newly added Hello button. If you click on it, nothing happens; that's normal because we haven't linked it to anything yet.

Creating the component’s initial structure

At this step, you will create a skeleton structure for your Hello World custom component. At the end of this step, you will be able to display a side panel and toggle it with the Hello button you created at the previous step, it should look like this:

Hello World side panel

  1. Add a new helloworld/ folder in src/components/

  2. In the helloworld/ folder, create three new files:

    • component.ts (which will contain the component's class definition)
    • template.html (which will contain the component's HTML template to be rendered)
    • style.css (which will contain the styling rules)

    Importing styles into a component
    • To import a single stylesheet, use the styleUrl property with a single filepath, e.g. styleUrl = "./style.css";

    • To import multiple stylesheets, use the styleUrls property with an array of filepaths, e.g. styleUrls = ['./style.css', '../../styles/common.css'];

    Geogirafe comes with a set of predefined styles, see the Styling documentation for more details.

  3. In helloworld/component.ts, copy the following code:

    import type { Callback } from "../../tools/state/statemanager";
    import GirafeHTMLElement from "../../base/GirafeHTMLElement";
    import I18nManager from "../../tools/i18n/i18nmanager";

    class HelloWorldComponent extends GirafeHTMLElement {
    templateUrl = "./template.html";
    styleUrls = ["./style.css", "../../styles/common.css"]; // we also import the common styles

    private readonly eventsCallbacks: Callback[] = [];
    darkFrontendMode: boolean = false;
    visible: boolean = false;
    i18nManager: I18nManager;

    constructor() {
    super("hello");

    // Import the i18n manager to handle translations
    this.i18nManager = I18nManager.getInstance();
    }

    // Renders the component or the empty component depending on visibility attribute
    render(): void {
    this.visible ? this.renderComponent() : this.renderEmptyComponent();
    }

    // Renders the component
    private renderComponent() {
    super.render();
    super.girafeTranslate();
    this.activateTooltips(false, [800, 0], "top-end");
    this.registerEvents();
    }

    // Hide the panel and removes listeners
    private renderEmptyComponent() {
    this.state.selection.enabled = true;

    // Unregister events
    this.unregisterEvents();

    // Render empty component
    this.renderEmpty();
    }

    // Register events
    registerEvents(): void {
    this.eventsCallbacks.push(
    // Subscribe to dark mode toggle
    this.subscribe(
    "interface.darkFrontendMode",
    (_oldValue: boolean, newValue: boolean) => {
    this.darkFrontendMode = newValue;
    }
    )
    );
    }

    // Unregister events
    unregisterEvents(): void {
    this.unsubscribe(this.eventsCallbacks);
    this.eventsCallbacks.length = 0;
    }

    // Register visibility events
    registerVisibilityEvents(): void {
    // Hello World panel toggle
    this.subscribe(
    "interface.helloPanelVisible",
    (_oldValue: boolean, newValue: boolean) => this.togglePanel(newValue)
    );
    }

    // Toggle side panel display
    private async togglePanel(visible: boolean): Promise<void> {
    this.visible = visible;
    this.render();
    }

    /*
    ConnectedCallback(): called each time the element is added to the document. The specification recommends that,
    as far as possible, developers should implement custom element setup
    in this callback rather than the constructor.
    */
    connectedCallback(): void {
    this.loadConfig().then(() => {
    this.render();
    super.girafeTranslate();
    this.registerVisibilityEvents();
    });
    }
    }

    export default HelloWorldComponent;
  4. In template.html, add the following HTML:

    <div id="panel">
    <div id="content">
    <h1 i18n="hello">Hello Giraffe!</h1>
    </div>
    </div>
    i18n translations

    Geogirafe uses the i18next internationalization framework to translate user interface elements.

    The i18n attribute in html elements is a reference to a variable in the i18n translation files located in the src/assets/i18n/ folder. In the preconfigured instance we are using, there are 4 files with the core translations: de.json (German), en.json (English), fr.json (French), it.json (Italian). Additional local or remote translation files can be included as needed.

    To access translation variables (in component.ts or template.html), Geogirafe also provides a I18nManager class with a getTranslation() method, e.g.:

    <div>${this.i18nManager.getTranslation('color_giraffe')}</div>

    The list of translations and source files must be configured in public/config.json, e.g.:

    "languages": {
    "translations": {
    "de": ["i18n/de.json", "i18n/custom-translation-de.json"],
    "en": ["i18n/en.json"],
    "fr": ["i18n/fr.json", "https://sitn.ne.ch/static/dummy/fr.json"],
    "it": ["i18n/it.json"]
    },
    "defaultLanguage": "en"
    },

    See the documentation on Configuration for more details.

  5. Create four new translation files the in the src/assets/i18n/ folder:

    helloworld-de.json for German:

    {
    "de": {
    "draw_giraffe": "Zeichnen",
    "draw_giraffe_help": "Zeichne eine Giraffe",
    "delete_all_giraffes": "Alles löschen",
    "delete_all_giraffes_help": "Lösche alle Giraffen",
    "zoom_on_giraffe": "Zoomen",
    "zoom_on_giraffe_help": "Zoomen Sie auf die Giraffe",
    "color_giraffe": "Klicken Sie hier, um meine Farbe zu ändern!",
    "hello": "Hallo Giraffe!",
    "hello-panel": "Meine Komponente"
    }
    }

    helloworld-en.json for English:

    {
    "en": {
    "draw_giraffe": "Draw",
    "draw_giraffe_help": "Draw a giraffe",
    "delete_all_giraffes": "Delete all",
    "delete_all_giraffes_help": "Delete all giraffes",
    "zoom_on_giraffe": "Zoom",
    "zoom_on_giraffe_help": "Zoom on giraffe",
    "color_giraffe": "Click to change my color!",
    "hello": "Hello giraffe!",
    "hello-panel": "My component"
    }
    }

    helloworld-fr.json for French:

    {
    "fr": {
    "draw_giraffe": "Dessiner",
    "draw_giraffe_help": "Dessiner une girafe",
    "delete_all_giraffes": "Supprimer tout",
    "delete_all_giraffes_help": "Supprimer toutes les girafes",
    "zoom_on_giraffe": "Zoomer",
    "zoom_on_giraffe_help": "Zoomer sur la girafe",
    "color_giraffe": "Clique pour changer ma couleur!",
    "hello": "Salut girafe!",
    "hello-panel": "Mon composant"
    }
    }

    helloworld-it.json for Italian:

    {
    "it": {
    "draw_giraffe": "Disegnare",
    "draw_giraffe_help": "Disegna una giraffa",
    "delete_all_giraffes": "Elimina tutte",
    "delete_all_giraffes_help": "Elimina tutte le giraffe",
    "zoom_on_giraffe": "Zoomer",
    "zoom_on_giraffe_help": "Zoom sulla giraffa",
    "color_giraffe": "Clicca per cambiare il mio colore!",
    "hello": "Ciao giraffa!",
    "hello-panel": "Il mio componente"
    }
    }
  6. Add the new translation files in the configuration (public/config.json):

    "languages": {
    "translations": {
    "de": ["i18n/de.json", "i18n/helloworld-de.json"],
    "en": ["i18n/en.json", "i18n/helloworld-en.json"],
    "fr": ["i18n/fr.json", "i18n/helloworld-fr.json", "https://sitn.ne.ch/static/dummy/fr.json"],
    "it": ["i18n/it.json", "i18n/helloworld-it.json"]
    },
    "defaultLanguage": "fr"
    },
  7. In style.css, add the following styling rules:

    #panel {
    height: 100%;
    overflow: auto;
    min-width: 20rem;
    }

    #content {
    background: var(--bkg-color);
    padding: 1.5rem;
    margin: 0;
    }

    #content .message {
    text-align: center;
    width: 100%;
    }

    #content .message * {
    margin: 0.25rem;
    }

    #content div.option {
    margin-top: 0.5rem;
    display: flex;
    justify-content: space-between;
    }

    .btn-group-flex {
    display: flex;
    flex-direction: row;
    gap: 0.5rem;
    margin: 0.5rem auto;
    }

    .btn-group-flex > button,
    .btn-group-flex > select,
    .btn-group-flex > input {
    flex: 50%;
    }
  8. In src/main.ts:

    Import the HelloWorldComponent by adding the following line to the list of imports at the beginning of the file:

    import HelloWorldComponent from "./components/helloworld/component";

    Define the component's name (alias) by adding the following line to the list of definitions on the customElements variable:

    customElements.define("girafe-helloworld", HelloWorldComponent);
  9. In index.html, add the girafe-helloworld custom element you just defined to the right panel element <girafe-lr-panel>:

    <girafe-lr-panel class="panel-right" dock="right">
    <girafe-drawing
    slot="main"
    data-toggle-path="interface.drawingPanelVisible"
    data-title="drawing-panel"
    ></girafe-drawing>
    <girafe-print
    slot="main"
    data-toggle-path="interface.printPanelVisible"
    data-title="print-panel"
    ></girafe-print>
    <girafe-cross-section-settings
    slot="main"
    data-toggle-path="interface.crossSectionPanelVisible"
    data-title="cross-section-settings"
    ></girafe-cross-section-settings>
    <girafe-helloworld
    slot="main"
    data-toggle-path="interface.helloPanelVisible"
    data-title="hello-panel"
    ></girafe-helloworld>
    <girafe-user-preferences
    slot="main"
    data-toggle-path="interface.userPreferencesPanelVisible"
    data-title="user-preferences-panel"
    ></girafe-user-preferences>
    </girafe-lr-panel>
    data-title attribute

    The data-title attribute in custom elements is a reference to a variable in the i18n translation files (in our example, the hello-panel variable). It is used to automatically translate the header at the top of the panel.

If you look in your browser and click on the Hello button, you should now see a side panel with a Salut girafe! title. If you try changing the language in the user settings menu, the default text will be automatically replaced by the translated text specified in the hello variable in the translation files.

Adding markers on the map

At this step, you will add a vector layer with markers to the map. The result should look like this (notice the three giraffe shaped markers on the map when the HelloWorld panel is open):

Hello World side panel

  1. In helloworld/component.ts at the beginning, update the list of imports:

    import type { Callback } from "../../tools/state/statemanager";
    import GirafeHTMLElement from "../../base/GirafeHTMLElement";
    import I18nManager from "../../tools/i18n/i18nmanager";
    import MapManager from "../../tools/state/mapManager";
    import Map from "ol/Map";
    import VectorSource from "ol/source/Vector";
    import VectorLayer from "ol/layer/Vector";
    import Feature from "ol/Feature";
    import { Icon, Style } from "ol/style";
    import { StyleLike } from "ol/style/Style";
    import { Point } from "ol/geom";
  2. Then, modify the HelloWorldComponent class:

    Add the typed properties definitions for the map, vector source (helloSource), and vector layer (helloLayer) at the beginning:

    class HelloWorldComponent extends GirafeHTMLElement {
    templateUrl = './template.html';
    styleUrl = './style.css';

    private readonly eventsCallbacks: Callback[] = [];
    darkFrontendMode: boolean = false;
    visible: boolean = false;
    i18nManager: I18nManager;
    private readonly map: Map;
    private helloSource: VectorSource<Feature<Geometry>> | null = null;
    private helloLayer: VectorLayer<VectorSource<Feature<Geometry>>> | null = null;

    In the constructor, import the map and i18n managers, define the vector source (helloSource) and the vector layer (helloLayer) properties:

     constructor() {
    super('hello');

    // Import the i18n manager to handle translations
    this.i18nManager = I18nManager.getInstance();

    // Import the open layers map instance from Geogirafe's map manager
    this.map = MapManager.getInstance().getMap();
    // Define a vector source containing three arbitrary point features
    this.helloSource = new VectorSource({
    features: [
    new Feature({ geometry: new Point([2554842.0, 1204042.0]), color: '#FF0000', selected: false }),
    new Feature({ geometry: new Point([2556508.3, 1195394.6]), color: '#00FF00', selected: false }),
    new Feature({ geometry: new Point([2537187.2, 1186086.9]), color: '#0000FF', selected: false })
    ] as Feature<Geometry>[],
    wrapX: false
    });

    // Define a vector layer
    this.helloLayer = new VectorLayer({
    source: this.helloSource as VectorSource<Feature<Geometry>>,
    style: this.myStyleFunction as StyleLike
    });

    // Add a name property to the vector layer
    this.helloLayer.set('name', 'helloworldLayer');

    }

    Before the constructor, add the following myStyleFunction() function to style the map markers (note that we use inline SVG to dynamically create giraffe shaped markers and vary their style depending on their selected property):

    // Marker styling function
    myStyleFunction(feature: Feature, _resolution: number): StyleLike {
    let color = feature.get('color');
    let strokeColor = '#000000';
    let size = 60;

    if (feature.get('selected')) {
    color = feature.get('color');
    strokeColor = '#FFFF00';
    size = 80;
    }

    // We use inline SVG to dynamically create giraffe shaped markers
    const svg = `<svg
    width="90.222916mm"
    height="90.222916mm"
    viewBox="0 0 90.222916 90.222916"
    version="1.1"
    xmlns="http://www.w3.org/2000/svg"
    xmlns:svg="http://www.w3.org/2000/svg">
    <g transform="translate(-14.574463,-98.921036)">
    <path
    fill="${color}"
    fill-opacity="1"
    stroke="${strokeColor}"
    stroke-width="1"
    stroke-opacity="1"
    stroke-miterlimit="4"
    stroke-dasharray="none"
    stroke-dashoffset="0"
    d="m 32.830713,176.09631 v -9.87264 l 2.377766,-3.42267 c 1.307771,-1.88247 2.381555,-3.60127 2.386187,-3.81955 0.0046,-0.21828 -0.664646,-2.55071 -1.487284,-5.18318 l -1.495704,-4.78631 0.857198,-2.22515 c 0.471459,-1.22383 1.448529,-3.74361 2.171265,-5.59952 l 1.314068,-3.37437 9.120679,-5.44227 9.120679,-5.44228 0.716011,-1.48395 c 0.709109,-1.46964 0.820541,-1.55747 11.563926,-9.11391 5.966354,-4.19648 11.830182,-8.33914 13.030729,-9.20591 l 2.182813,-1.57595 v -2.00472 -2.00471 l 1.389062,0.08 1.389063,0.08 0.07761,2.06933 0.07761,2.06933 3.847833,4.66572 3.847835,4.66571 -0.88274,0.73184 c -0.485507,0.40252 -0.943475,0.7055 -1.017706,0.67329 -0.07423,-0.0322 -2.016871,-1.05279 -4.316976,-2.26796 -2.300108,-1.21518 -4.214889,-2.16768 -4.255074,-2.11667 -0.04018,0.051 -5.325184,6.41777 -11.744447,14.14836 l -11.671387,14.05561 0.497419,4.33293 0.497417,4.33293 -0.929169,3.73054 -0.929172,3.73054 2.629302,4.86842 2.629302,4.86841 0.706361,9.12813 c 0.388498,5.02047 0.703884,9.46239 0.700855,9.87094 -0.0052,0.70765 -0.05778,0.73897 -1.10975,0.66146 l -1.104244,-0.0814 -1.560949,-8.59896 -1.560951,-8.59896 -1.320496,-2.30995 c -0.726271,-1.27048 -1.424295,-2.37411 -1.55116,-2.45252 -0.329001,-0.20333 -0.456509,1.07325 -1.057089,10.58331 -0.294077,4.65666 -0.604954,9.15127 -0.690838,9.98802 l -0.156152,1.52135 h -1.262986 -1.262983 l -0.161502,-3.37344 c -0.431204,-9.00687 -0.468421,-20.04627 -0.07782,-23.08489 l 0.391123,-3.04271 -1.318236,-2.18281 c -0.72503,-1.20055 -1.413383,-2.18281 -1.529676,-2.18281 -0.247655,0 -5.318863,6.12014 -7.454029,8.99583 -3.989452,5.37308 -5.223464,7.10646 -5.748154,8.07425 -0.472602,0.87172 -0.708762,2.35758 -1.366317,8.59653 -0.437228,4.14846 -0.794959,7.69057 -0.794959,7.87135 0,0.23058 -0.552906,0.3287 -1.852084,0.3287 h -1.852083 z m 56.091666,-66.57304 c 0,-0.37738 -0.785797,-1.07723 -1.209524,-1.07723 -0.382265,0 -0.52669,0.82142 -0.201586,1.14652 0.267218,0.26722 1.41111,0.21105 1.41111,-0.0693 z m -42.566717,75.85037 c -0.548121,-1.48126 -5.29246,-15.72432 -5.369167,-16.11886 -0.08427,-0.43345 4.749386,-7.59748 5.13953,-7.61738 0.109141,-0.006 0.198438,2.55202 0.198438,5.6835 v 5.69362 l 1.852083,6.24487 c 1.018646,3.43468 1.852083,6.34942 1.852083,6.47721 0,0.1278 -0.776853,0.23235 -1.72634,0.23235 -1.548619,0 -1.749017,-0.0613 -1.946627,-0.59531 z"
    />
    </g>
    </svg>`;

    return new Style({
    image: new Icon({
    anchor: [0.5, 0.5],
    anchorXUnits: 'fraction',
    anchorYUnits: 'fraction',
    height: size,
    width: size,
    src: `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`
    })
    });
    }

    In the renderComponent() method, add the following code to include the HelloLayer on the map:

    // Renders the component
    private renderComponent() {
    super.render();
    super.girafeTranslate();
    this.activateTooltips(false, [800, 0], 'top-end');
    this.registerEvents();

    // Check if HelloLayer is already included in the map
    const existsHelloLayer = this.map.getLayers().getArray().includes(this.helloLayer!);
    if (!existsHelloLayer) {
    this.map.addLayer(this.helloLayer!);
    }
    }

    In the renderEmptyComponent() method, add the following code to remove the HelloLayer from the map:

    // Hide the panel and removes listeners
    private renderEmptyComponent() {
    this.state.selection.enabled = true;

    // Unregister events
    this.unregisterEvents();

    // Remove helloLayer from map
    this.map.removeLayer(this.helloLayer!);

    // Render empty component
    this.renderEmpty();
    }

Look in your browser and open the HelloWorld panel, you should now see three colored giraffes on the map. If you close the panel, the giraffes should dissapear.

Adding select, draw and modify interactions to the map markers

At this step, you will add selection, drawing, modifying (moving) interactions to the map markers. You will also add a button to the HelloWorld panel to toggle the drawing mode and another one to delete all the markers. The result will look like this:

Hello World side panel

  1. In helloworld/component.ts at the beginning, update the list of imports:

    import type { Callback } from "../../tools/state/statemanager";
    import GirafeHTMLElement from "../../base/GirafeHTMLElement";
    import I18nManager from "../../tools/i18n/i18nmanager";
    import MapManager from "../../tools/state/mapManager";
    import { v4 as uuidv4 } from "uuid";
    import Map from "ol/Map";
    import VectorSource from "ol/source/Vector";
    import VectorLayer from "ol/layer/Vector";
    import Feature from "ol/Feature";
    import { Icon, Style } from "ol/style";
    import { StyleLike } from "ol/style/Style";
    import { Point } from "ol/geom";
    import { Geometry } from "ol/geom";
    import { Select, Draw, Modify } from "ol/interaction";
    import { SelectEvent } from "ol/interaction/Select";
    import { DrawEvent } from "ol/interaction/Draw";
    import { ModifyEvent } from "ol/interaction/Modify";
    import { click } from "ol/events/condition";
    import { ObjectEvent } from "ol/Object";
  2. Then, modify the HelloWorldComponent class:

    Add the typed properties definitions for the interactions (selectInteraction, drawInteraction, modifyInteraction) and for the drawing mode toogle (draw) at the beginning:

    class HelloWorldComponent extends GirafeHTMLElement {
    templateUrl = './template.html';
    styleUrls = ['./style.css', '../../styles/common.css']; // we also import the common styles

    private readonly eventsCallbacks: Callback[] = [];
    darkFrontendMode: boolean = false;
    visible: boolean = false;
    private readonly map: Map;
    private helloSource: VectorSource<Feature<Geometry>> | null = null;
    private helloLayer: VectorLayer<VectorSource<Feature<Geometry>>> | null = null;
    selectInteraction: Select;
    drawInteraction: Draw;
    modifyInteraction: Modify;
    draw: boolean = false;

    In the constructor, remove the three sample markers in the helloSource, we won't need them anymore:

    // Define a vector source containing three arbitrary point features
    this.helloSource = new VectorSource({
    features: [] as Feature<Geometry>[],
    wrapX: false,
    });

    In the constructor, initialize a select interaction (which will allow you to select markers on the map) and add an event listener to detect when a selection occurs:

    // Define selection interaction
    this.selectInteraction = new Select({
    condition: click,
    layers: [this.helloLayer], // Layers from which features should be selected
    });

    // Listen to select event
    this.selectInteraction.on("select", (_e: SelectEvent) => {
    console.log("a marker was selected");

    // set cursor style to default on selection
    const target: HTMLElement = this.map.getTarget() as HTMLElement;
    target.style.cursor = "default";

    const selected: Feature[] = _e.selected;

    // set selected property to false for all features
    this.helloSource!.forEachFeature((f: Feature<Geometry>) => {
    f.set("selected", false);
    });

    // set selected property to true for selected features
    selected.forEach((f: Feature<Geometry>) => {
    f.set("selected", true);
    });
    });

    In the constructor, initialize a draw interaction (which will allow you to draw new markers on the map) and add event listeners (drawstart and drawend) to detect when a drawing is made:

    // Define draw interaction
    this.drawInteraction = new Draw({
    source: this.helloSource!,
    type: "Point",
    });

    // Listen to drawing start event
    this.drawInteraction.on("drawstart", (_e: DrawEvent) => {
    this.helloSource?.forEachFeature((f) => {
    f.set("selected", false);
    });
    });

    // Listen to drawing end event
    this.drawInteraction.on("drawend", (_e: DrawEvent) => {
    // NOTE: drawend event is dispatched before inserting the features
    const color = randomColor();
    _e.feature.setId(uuidv4()); // set marker ID
    _e.feature.set("color", color); // set marker color (random)
    _e.feature.set("selected", true); // set marker selected status
    _e.feature.set("timestamp", Date.now()); // set marker timestamp
    _e.feature.setStyle(this.myStyleFunction(_e.feature, 0)); // set marker style function

    _e.feature.on("propertychange", (ev: ObjectEvent) => {
    _e.feature.setStyle(this.myStyleFunction(ev.target as Feature, 0));
    });
    });

    In the constructor, initialize a modify interaction (which will allow you to move the markers on the map) and add event listeners (modifystart and modifyend) to detect when a marker is moved:

    // Define modify interaction
    this.modifyInteraction = new Modify({
    hitDetection: this.helloLayer,
    source: this.helloSource,
    style: new Style(), // Use an empty style for the modification point or vertex
    });

    // Listen to modification start event
    this.modifyInteraction.on("modifystart", (_e: ModifyEvent) => {
    const target: HTMLElement = this.map.getTarget() as HTMLElement;
    target.style.cursor = "grabbing"; // change cursor to grabbing when marker starts moving
    });

    // Listen to modification end event
    this.modifyInteraction.on("modifyend", (_e: ModifyEvent) => {
    const target: HTMLElement = this.map.getTarget() as HTMLElement;
    target.style.cursor = "default"; // change cursor to default when marker stop moving
    });

    In the constructor, add the selection interaction to the map and enable the drawing mode:

    // Add select interaction to the map
    this.map.addInteraction(this.selectInteraction);

    // Enable drawing mode
    this.setDraw(true);

    In renderEmptyComponent(), add calls to remove interactions :

      // Hide the panel and removes listeners
    private renderEmptyComponent() {
    this.state.selection.enabled = true;

    // Turn off drawing mode
    this.setDraw(false);

    // Unregister events
    this.unregisterEvents();

    // Remove helloLayer from map
    this.map.removeLayer(this.helloLayer!);

    // Render empty component
    this.renderEmpty();
    }

    After the constructor, add the following method to set the drawing mode:

    // Set drawing mode
    setDraw(val: boolean): void {
    this.draw = val;
    if (val) {
    this.map.addInteraction(this.drawInteraction);
    this.map.addInteraction(this.modifyInteraction);
    } else {
    this.map.removeInteraction(this.drawInteraction);
    this.map.removeInteraction(this.modifyInteraction);
    }
    super.render();
    }

    After the constructor, add the following method to delete all markers:

    // Delete all features
    deleteAllFeatures(): void {
    this.helloSource?.clear();
    }

    Create a new file utils.ts in the helloworld/ directory and add the following randomColor() function to generate random colors (which will be used to color the newly drawn map markers):

    // Random color generator function
    export function randomColor(): string {
    const letters = "0123456789ABCDEF";
    let color = "#";
    for (let i = 0; i < 6; i++) {
    color += letters[Math.floor(Math.random() * 16)];
    }
    return color;
    }

    In helloworld/component.html, import the randomColor() function from utils.ts:

    import type { Callback } from "../../tools/state/statemanager";
    import GirafeHTMLElement from "../../base/GirafeHTMLElement";
    import I18nManager from "../../tools/i18n/i18nmanager";
    import MapManager from "../../tools/state/mapManager";
    import { v4 as uuidv4 } from "uuid";
    import Map from "ol/Map";
    import VectorSource from "ol/source/Vector";
    import { VectorSourceEvent } from "ol/source/Vector";
    import VectorLayer from "ol/layer/Vector";
    import Feature from "ol/Feature";
    import { Icon, Style } from "ol/style";
    import { StyleLike } from "ol/style/Style";
    import { Point } from "ol/geom";
    import { Geometry } from "ol/geom";
    import { Select, Draw, Modify } from "ol/interaction";
    import { SelectEvent } from "ol/interaction/Select";
    import { DrawEvent } from "ol/interaction/Draw";
    import { ModifyEvent } from "ol/interaction/Modify";
    import { click } from "ol/events/condition";
    import { ObjectEvent } from "ol/Object";
    import { randomColor } from "./utils";
  3. In the template (helloworld/template.html) of the helloWorld component, add a button to toggle the drawing mode and another one to delete all markers:

    templating syntax

    Geogirafe uses the µhtml templating engine to render HTML. You can dynamically change the content and attributes (style, title, etc) of an element by assigning variables or expressions with the curly brackets syntax ${}. For example, you can use:

    • the class attribute to dynamically change the style of an element.

    • the title attribute to display a dynamic tooltip

    You can also attach event listeners (and the associated callback function) to an element:

    • the onclick attribute is used to define a callback function that runs when the button is clicked.

    For more details, please see the µhtml documentation.

    <div id="panel">
    <div id="content">
    <h1 i18n="hello">Hello World!</h1>

    <!-- Button group -->
    <div class="btn-group-flex">
    <!-- Draw button -->
    <button
    id="draw-btn"
    class="${this.draw ? 'gg-button active' : 'gg-button'}"
    title="${this.i18nManager.getTranslation('draw_giraffe_help')}"
    onclick="${() => this.setDraw(!this.draw)}"
    i18n="draw_giraffe"
    >
    Draw
    </button>
    <!-- Delete all button -->
    <button
    id="delete-points-btn"
    class="gg-button"
    title="${this.i18nManager.getTranslation('delete_all_giraffes_help')}"
    onclick="${() => this.deleteAllFeatures()}"
    i18n="delete_all_giraffes"
    >
    Delete
    </button>
    </div>
    </div>
    </div>

Look in your browser and open the HelloWorld panel, you should now see draw and delete buttons. If you click on the draw button, it will activate the drawing mode and you can then click on the map to add markers. You can also select and move markers on the map (check the console to see the select and modify event logs). When you select a marker, its style will be updated: it will be larger and have a yellow outline. If you click on the delete all button, all the markers on the map will be deleted.

Using the state manager

In this step, you will learn how to use the state manager to store the helloWorld component's state variables. The result will look like this:

Hello World side panel

State management

Geogirafe uses a simple state manager based on Javascript Proxy objects, via the on-change library.

Any component that extends the <GirafeHTMLElement> class automatically inherits access to Geogirafe's state manager (via this.state). By convention, Geogirafe uses two locations to store a component's state variables:

  • this.state for core components, e.g. this.state.themes, state.basemaps, state.mouseCoordinates, defined in src/tools/state/state.ts.
  • this.state.extendedState for custom/addon components, e.g. this.state.extendedState.helloworld, defined directly in custom components.

Observing changes on a state variable is done via the this.subscribe() method:

this.subscribe(
"path to variable in state manager or regular expression",
(_oldPropVal: any, _newPropVal: any, _parent: parentType) => {
// callback function executed when a change on the observed variable is detected
}
);

For example, to observe changes in the mouse coordinates variable in the core state (this.state.mouseCoordinates), we use:

this.subscribe(
"mouseCoordinates",
(_oldCoordinates: number[], _newCoordinates: number[]) => {
console.log(_newCoordinates);
}
);

The state manager is also able to detect deep changes in properties (i.e. multiple levels of nesting). If we wanted to monitor change only on a specific property of an an object in an array, you can use a regular expression with wildcards:

// Note that we use a regular expression with wildcards (*) here
this.subscribe(
/extendedState\.helloworld\.markers\..*\.myproperty/,
(_oldPropVal: boolean, _newPropVal: boolean, _parent: MyObjectType) => {
// callback function executed when a change on the observed variable is detected
console.log(
`myproperty on parent object ${_parent.id} changed from ${_oldPropVal} to ${_newPropVal}`
);
}
);

To observe changes on the length of an array (i.e. e.g. when an item is added or deleted), you can use:

// Note that we use a string containing the path to the marker property here
this.stateManager.subscribe(
"extendedState.helloworld.myarray",
(
_oldPropVal: MyObjectType[],
_newPropVal: MyObjectType[],
_parent: HelloState
) => {
// callback function exectuted when a change on the observed variable is detected
}
);

We start by definining two typescript classes to represent the map markers (Marker) and the state (HelloState) of the helloWorld component:

  1. In the helloWorld component's root folder, create a new file called hellostate.ts:

    export class Marker {
    id: string = "";
    coordinates: number[] = [];
    color: string = "";
    selected: boolean = false;
    }

    export class HelloState {
    draw: boolean = false;
    markers: Marker[] = [];
    }
  2. In helloworld/component.ts, add an import for the OpenLayers VectorSourceEvent classe and for the HelloState and Marker classes you just created:

    import type { Callback } from "../../tools/state/statemanager";
    import GirafeHTMLElement from "../../base/GirafeHTMLElement";
    import I18nManager from "../../tools/i18n/i18nmanager";
    import MapManager from "../../tools/state/mapManager";
    import { v4 as uuidv4 } from "uuid";
    import Map from "ol/Map";
    import VectorSource from "ol/source/Vector";
    import { VectorSourceEvent } from "ol/source/Vector";
    import VectorLayer from "ol/layer/Vector";
    import Feature from "ol/Feature";
    import { Icon, Style } from "ol/style";
    import { StyleLike } from "ol/style/Style";
    import { Point } from "ol/geom";
    import { Geometry } from "ol/geom";
    import { Select, Draw, Modify } from "ol/interaction";
    import { SelectEvent } from "ol/interaction/Select";
    import { DrawEvent } from "ol/interaction/Draw";
    import { ModifyEvent } from "ol/interaction/Modify";
    import { click } from "ol/events/condition";
    import { ObjectEvent } from "ol/Object";
    import { randomColor } from "./utils";
    import { HelloState, Marker } from "./hellostate";
  3. Then, modify the HelloWorldComponent class:

    Add a helloworldState property of type HelloState to the property definitions at the beggining of HelloWorldComponent and remove the draw property (we will move it to the component's state manager):

    class HelloWorldComponent extends GirafeHTMLElement {
    templateUrl = './template.html';
    styleUrls = ['./style.css', '../../styles/common.css']; // we also import the common styles

    private readonly eventsCallbacks: Callback[] = [];
    darkFrontendMode: boolean = false;
    visible: boolean = false;
    i18nManager: I18nManager;
    private readonly map: Map;
    private helloSource: VectorSource<Feature<Geometry>> | null = null;
    private helloLayer: VectorLayer<VectorSource<Feature<Geometry>>> | null = null;
    selectInteraction: Select;
    drawInteraction: Draw;
    modifyInteraction: Modify;
    helloworldState: HelloState;

    In the constructor, initialize the helloworldState property with a reference to Geogirafe's extended state manager, remove the old draw (this.draw) property and instead initialize the draw property to false in the extended state manager:

     constructor() {
    super('hello');

    // Import the open layers map instance from Geogirafe's map manager
    this.map = MapManager.getInstance().getMap();

    // Initialize HelloState in Geogirafe's extended state manager
    this.state.extendedState.helloworld = new HelloState();

    // Reference the global state manager (this.state.extendedState.helloworld) from the local state manager (this.helloworldState)
    this.helloworldState = this.state.extendedState.helloworld as HelloState;

    // Initialize the drawing mode to off (false) in the extended state manager
    this.helloworldState.draw = false;

  4. In the constructor, after the initialization of helloSource, add the following event listeners on helloSource as following:

    // Define a vector source containing three arbitrary point features
    this.helloSource = new VectorSource({
    features: [] as Feature<Geometry>[],
    wrapX: false,
    });

    // Listen to vector source feature adding
    this.helloSource.on("addfeature", (_e: VectorSourceEvent) => {
    const newFeature: Feature<Point> = _e.feature as Feature<Point>;
    const newMarker = new Marker();
    newMarker.id = newFeature.getId() as string;
    newMarker.coordinates = newFeature.getGeometry()!.getCoordinates();
    newMarker.color = newFeature.get("color") as string;
    newMarker.selected = newFeature.get("selected") as boolean;

    this.helloworldState.markers.push(newMarker);
    super.render();
    });

    // Listen to vector source feature removal
    this.helloSource.on("removefeature", (_e: VectorSourceEvent) => {
    // Remove marker from list in state manager
    const index = this.helloworldState.markers.findIndex(
    (x) => x.id === _e.feature?.getId()
    );
    if (index > -1) {
    this.helloworldState.markers.splice(index, 1);
    }
    super.render();
    });

    // Listen to vector source feature changes
    this.helloSource.on("changefeature", (_e: VectorSourceEvent) => {
    // Update marker attributes in state manager
    for (let i = 0; i < this.helloworldState.markers.length; i++) {
    const f = this.helloworldState.markers[i] as Marker;

    // Check if feature exists in vector source
    const olFeature = this.helloSource?.getFeatureById(
    f.id
    ) as Feature<Point>;
    // Update changed feature properties in the state manager
    if (olFeature) {
    f.color = olFeature.get("color");
    f.coordinates = olFeature.getGeometry()!.getCoordinates();
    f.selected = olFeature.get("selected");
    }
    }
    super.render();
    });
  5. In the template (helloworld/template.html) of the helloWorld component, add a stringified list of the markers stored in the extended state manager and replace this.draw by this.helloworldState.draw:

    <div id="panel">
    <div id="content">
    <h1 i18n="hello">Hello World!</h1>

    <!-- Stringified list of markers from the state manager -->
    <div>${JSON.stringify(this.helloworldState.markers)}</div>

    <!-- Button group -->
    <div class="btn-group-flex">
    <!-- Draw button -->
    <button
    id="draw-btn"
    class="${this.helloworldState.draw ? 'gg-button active' : 'gg-button'}"
    title="${this.i18nManager.getTranslation('draw_giraffe_help')}"
    id="draw-btn"
    onclick="${() => this.setDraw(!this.helloworldState.draw)}"
    i18n="draw_giraffe">
    Draw
    </button>

    <!-- Delete all button -->
    <button
    id="delete-points-btn"
    class="gg-button"
    title="${this.i18nManager.getTranslation('delete_all_giraffes_help')}"
    onclick="${() => this.deleteAllFeatures()}"
    i18n="delete_all_giraffes">
    Delete
    </button>
    </div>
    </div>
    </div>
  6. In the setDraw() method, replace all calls to this.draw by this.helloworldState.draw:

    // Set drawing mode
    setDraw(val: boolean): void {
    this.helloworldState.draw = val;
    if (val) {
    this.map.addInteraction(this.drawInteraction);
    this.map.addInteraction(this.modifyInteraction);
    } else {
    this.map.removeInteraction(this.drawInteraction);
    this.map.removeInteraction(this.modifyInteraction);
    }
    super.render();
    }

Look in your browser and open the HelloWorld panel. When you draw new markers, modify them or delete them (with the delete button), you should see the stringified state updated in the side panel.

Creating a subcomponent in the side panel to display a list of chips

In this step, we will create a subcomponent called "hellochip" and use it to display a list of chips in the HelloWorld side panel component. Each chip represents a map marker, as illustrated below:

Hellochips subcomponent

  1. Copy the giraffe.svg file to the project's public/icons/ folder

  2. Add a new hellochip/ folder in src/components/helloworld

  3. In the hellochip/ folder, create three new files:

    • component.ts (which will contain the component's class definition)
    • template.html (which will contain the component's HTML template to be rendered)
    • style.css (which will contain the styling rules)
  4. In helloworld/hellochip/component.ts, copy the following code:

    import type { Callback } from "../../../tools/state/statemanager";
    import GirafeHTMLElement from "../../../base/GirafeHTMLElement";
    import { HelloState, Marker } from "../hellostate";

    class HelloChipComponent extends GirafeHTMLElement {
    templateUrl = "./template.html";
    styleUrl = "./style.css";

    private readonly eventsCallbacks: Callback[] = [];
    fid: string = this.getAttribute("fid") as string;
    color: string = this.getAttribute("color") as string;
    mystyle: string = "";
    title: string = "";
    className: string = "chip";

    helloworldState: HelloState;

    constructor() {
    super("hellochip");
    this.helloworldState = this.state.extendedState.helloworld as HelloState;
    }

    // Select a giraffe
    select(): void {
    console.log(`selected giraffe ${this.fid}`);
    for (let i = 0; i < this.helloworldState.markers.length; i++) {
    if (this.helloworldState.markers[i].id === this.fid) {
    this.helloworldState.markers[i].selected = true;
    } else {
    this.helloworldState.markers[i].selected = false;
    }
    }
    }

    // Renders the component
    private renderComponent(): void {
    this.color = this.getAttribute("color") as string;
    this.fid = this.getAttribute("fid") as string;
    this.mystyle = `background-color:${this.color};`;
    this.title = `Click to select giraffe ${this.fid}`;

    super.render();
    super.girafeTranslate();
    this.activateTooltips(false, [800, 0], "top-end");
    }

    // Register events
    registerEvents() {
    this.eventsCallbacks.push(
    this.stateManager.subscribe(
    /extendedState\.helloworld\.markers\..*\..*/,
    (
    _oldSelected: boolean,
    _newSelected: boolean,
    featureProps: Marker
    ) => {
    // update chip style when a map marker is selected
    if (featureProps.id === this.fid) {
    this.className = featureProps.selected
    ? "chip selected"
    : "chip";
    this.renderComponent();
    }
    }
    )
    );

    // Instead of using the onclick attribute in the html template, you may also declare event listeners here
    /*
    this.addEventListener('click', (_e: Event) => {
    this.select();
    });
    */
    }

    /*
    ConnectedCallback(): called each time the element is added to the document. The specification recommends that,
    as far as possible, developers should implement custom element setup
    in this callback rather than the constructor.
    */
    connectedCallback() {
    this.loadConfig().then(() => {
    this.renderComponent();
    super.girafeTranslate();
    this.registerEvents();
    });
    }
    }

    export default HelloChipComponent;
  5. In helloworld/hellochip/template.html, copy the following code:

    <img src="icons/giraffe.svg" title="${this.title}" width="32" height="32" class="${this.className}" style="${this.mystyle}" onclick="${() => this.select()}"></img>
  6. In helloworld/hellochip/style.css, copy the following code:

    .chip {
    border: 2px rgba(0, 0, 0, 0.192) solid;
    padding: 1px;
    cursor: pointer;
    box-shadow: 1px 1px 5px #00000042;
    }

    .chip:hover {
    border: 2px rgb(255, 238, 0) solid;
    box-shadow: 1px 1px 5px #e8f801;
    }

    .selected {
    border: 2px rgb(255, 238, 0) solid !important;
    }
  7. Modify helloworld/template.html, as following, to render the list of chips:

    <div id="panel">
    <div id="content">
    <h1 i18n="hello">Hello World!</h1>

    <!-- Stringified list of markers from the state manager -->
    <div>${JSON.stringify(this.helloworldState.markers)}</div>

    <!-- Button group -->
    <div class="btn-group-flex">
    <!-- Draw button -->
    <button
    id="draw-btn"
    class="${this.helloworldState.draw ? 'gg-button active' : 'gg-button'}"
    title="${this.i18nManager.getTranslation('draw_giraffe_help')}"
    onclick="${() => this.setDraw(!this.helloworldState.draw)}"
    i18n="draw_giraffe"
    >
    Draw
    </button>

    <!-- Delete all button -->
    <button
    id="delete-points-btn"
    class="gg-button"
    title="${this.i18nManager.getTranslation('delete_all_giraffes_help')}"
    onclick="${() => this.deleteAllFeatures()}"
    i18n="delete_all_giraffes"
    >
    Delete
    </button>
    </div>

    <!-- List of chips -->
    <div class="chip-container">
    ${ this.helloworldState.markers.map(f => uHtmlFor(f,
    f.id)`<girafe-hello-chip
    fid="${f.id}"
    color="${f.color}"
    ></girafe-hello-chip
    >`) }
    </div>
    </div>
    </div>
  8. In helloworld/component.ts, add the following subscriptions to the eventCallbacks array in the registerEvents() method. These subscriptions will listen to changes in the list of markers and their properties (color, selected) and run the callback functions:

    // Register events
    registerEvents(): void {
    this.eventsCallbacks.push(
    // Subscribe to dark mode toggle
    this.stateManager.subscribe('interface.darkFrontendMode', (_oldValue: boolean, newValue: boolean) => {
    this.darkFrontendMode = newValue;
    }),

    // Subscribe to property changes (color, selected, etc) on the markers
    this.stateManager.subscribe(
    /extendedState\.helloworld\.markers\..*\..*/,
    (_oldSelected: boolean, _newSelected: boolean, _marker: Marker) => {
    const mapFeatures = this.helloSource!.getFeatures();

    // Update feature properties on map
    if (mapFeatures) {
    for (let i = 0; i < mapFeatures.length; i++) {
    const target = this.helloworldState.markers.find((x) => x.id === mapFeatures[i].getId());
    if (target) {
    mapFeatures[i].set('color', target.color);
    mapFeatures[i].set('selected', target.selected);
    }
    }
    }
    }
    ),

    // Subscribe to changes on the list of markers (length change)
    this.stateManager.subscribe(
    'extendedState.helloworld.markers',
    (_oldMarkers: Marker[], _newMarkers: Marker[], _marker: Marker) => {
    // Iterate over map features to check if they should be rendered
    const mapFeatures = this.helloSource!.getFeatures();
    const removeFeatures: Feature<Geometry>[] = [];

    // Remove deleted features on map
    if (mapFeatures) {
    for (let i = 0; i < mapFeatures.length; i++) {
    const target = this.helloworldState.markers.find((x) => x.id === mapFeatures[i].getId());
    if (!target) {
    removeFeatures.push(mapFeatures[i]);
    }
    }
    }

    this.helloSource?.removeFeatures(removeFeatures);
    super.render();
    }
    )
    );
    }
  9. In src/main.ts:

    Import the HelloChipComponent by adding the following line to the list of imports at the beginning of the file:

    import HelloChipComponent from "./components/helloworld/hellochip/component";

    Define the component's name (alias) by adding the following line to the list of definitions on the customElements variable:

    customElements.define("girafe-hello-chip", HelloChipComponent);

Look in your browser and open the HelloWorld panel. When you draw new markers, modify them or delete them, you should see a list of colored chips rendered in the side panel.

Creating a subcomponent in the side panel to display and modify the selected marker's details

In this step, we will add a subcomponent called "hellodetails" to the HelloWorld side panel. This subcomponent will show an information box with details on the selected map marker and allow users to zoom a marker or delete it. The result is illustrated below:

Hellodetails subcomponent

  1. Add a new hellodetails/ folder in src/components/helloworld

  2. In the hellodetails/ folder, create three new files:

    • component.ts (which will contain the component's class definition)
    • template.html (which will contain the component's HTML template to be rendered)
    • style.css (which will contain the styling rules)
  3. In helloworld/hellodetails/component.ts, copy the following code:

    import type { Callback } from "../../../tools/state/statemanager";
    import GirafeHTMLElement from "../../../base/GirafeHTMLElement";
    import I18nManager from "../../../tools/i18n/i18nmanager";
    import MapManager from "../../../tools/state/mapManager";
    import Map from "ol/Map";
    import { Feature } from "ol";
    import { Geometry, Point } from "ol/geom";
    import VectorLayer from "ol/layer/Vector";
    import VectorSource from "ol/source/Vector";
    import { randomColor } from "../utils";
    import { HelloState, Marker } from "../hellostate";

    class HelloDetailsComponent extends GirafeHTMLElement {
    templateUrl = "./template.html";
    styleUrls = ["./style.css", "../../../styles/common.css"]; // we also import the common styles

    private readonly eventsCallbacks: Callback[] = [];
    private readonly map: Map;
    i18nManager: I18nManager;

    fid: string = "";
    color: string = "";
    coordinates: number[] = [];
    colorButton: HTMLElement;
    zoomButton: HTMLButtonElement;
    deleteButton: HTMLButtonElement;
    helloworldState: HelloState;

    constructor() {
    super("hellodetails");

    // Import the i18n manager to handle translations
    this.i18nManager = I18nManager.getInstance();

    // Import the open layers map instance from Geogirafe's map manager
    this.map = MapManager.getInstance().getMap();

    this.colorButton = this.shadow.getElementById(
    "giraffe-avatar"
    ) as HTMLElement;
    this.zoomButton = this.shadow.getElementById(
    "zoom-btn"
    )! as HTMLButtonElement;
    this.deleteButton = this.shadow.getElementById(
    "delete-btn"
    )! as HTMLButtonElement;
    this.helloworldState = this.state.extendedState.helloworld as HelloState;
    }

    // Zoom to feature
    zoomTo(id: string): void {
    const layers = this.map.getLayers().getArray();
    const helloLayer = layers.find(
    (layer) => layer.get("name") == "helloworldLayer"
    ) as VectorLayer<VectorSource<Feature<Geometry>>>;

    const source = helloLayer.getSource() as VectorSource;
    const feature: Feature<Point> = source.getFeatureById(
    id
    ) as Feature<Point>;

    if (feature) {
    this.state.position.center = feature.getGeometry()!.getCoordinates();
    this.state.position.zoom = 7;
    }
    }

    render(): void {
    this.renderComponent();
    }

    // Render the component
    private renderComponent(): void {
    const target = this.helloworldState.markers.find((x) => x.selected);

    if (target) {
    this.color = target.color as string;
    this.fid = target.id as string;
    this.coordinates = target.coordinates;
    } else {
    this.color = "";
    this.fid = "";
    this.coordinates = [];
    }

    super.render();
    super.girafeTranslate();
    this.activateTooltips(false, [800, 0], "top-end");

    this.colorButton = this.shadow.getElementById(
    "giraffe-avatar"
    )! as HTMLElement;
    this.zoomButton = this.shadow.getElementById(
    "zoom-btn"
    )! as HTMLButtonElement;
    this.deleteButton = this.shadow.getElementById(
    "delete-btn"
    )! as HTMLButtonElement;
    }

    // Register events
    registerEvents() {
    this.eventsCallbacks.push(
    this.stateManager.subscribe(
    "extendedState.helloworld.markers",
    (
    _oldSelected: boolean,
    _newSelected: boolean,
    _featureProps: Marker
    ) => {
    this.render();
    }
    ),
    this.stateManager.subscribe(
    /extendedState\.helloworld\.markers\..*\..*/,
    (
    _oldSelected: boolean,
    _newSelected: boolean,
    _featureProps: Marker
    ) => {
    this.fid = _featureProps.id;
    this.color = _featureProps.color;
    this.coordinates = _featureProps.coordinates;
    this.render();
    }
    )
    );

    // Change giraffe color button event listener
    this.colorButton.addEventListener("click", (_e) => {
    const target = this.helloworldState.markers.find((x) => x.selected);
    if (target) {
    target.color = randomColor();
    }
    });

    // Delete giraffe button event listener
    this.zoomButton.addEventListener("click", (_e: Event) => {
    this.zoomTo(this.fid);
    });

    // Delete giraffe button event listener
    this.deleteButton.addEventListener("click", (_e: Event) => {
    const index = this.helloworldState.markers.findIndex(
    (x) => x.id === this.fid
    );
    if (index > -1) {
    this.helloworldState.markers.splice(index, 1);
    }
    });
    }

    /*
    ConnectedCallback(): called each time the element is added to the document. The specification recommends that,
    as far as possible, developers should implement custom element setup
    in this callback rather than the constructor.
    */
    connectedCallback() {
    this.loadConfig().then(() => {
    this.render();
    super.girafeTranslate();
    this.registerEvents();
    });
    }
    }

    export default HelloDetailsComponent;
  4. In helloworld/hellodetails/template.html, copy the following code:

    <div id="hello-details">
    <div class="centered">
    <svg
    id="giraffe-avatar"
    width="240mm"
    height="240mm"
    viewBox="0 0 240 240"
    version="1.1"
    xmlns="http://www.w3.org/2000/svg"
    xmlns:svg="http://www.w3.org/2000/svg"
    >
    <g id="avatar-group" transform="translate(54.744042,15.699027)">
    <g transform="translate(0,-5.0919451)">
    <circle
    id="circle-primitive"
    fill="#FFFFFF"
    fill-opacity="1"
    stroke="none"
    cx="65.255959"
    cy="109.39291"
    r="94.908058"
    />
    <path
    id="giraffe-path"
    fill="${this.color}"
    fill-opacity="1"
    stroke="none"
    d="m 78.96013,58.932266 c -3.314919,-0.130721 -6.576367,1.217866 -8.697661,3.821988 -1.54944,1.902081 -2.220341,3.551907 -2.491322,6.127273 -0.360918,3.430104 1.579676,7.499835 4.662765,9.779768 l 0.711068,0.526065 -0.866613,4.357873 c -0.476589,2.39681 -0.925256,4.795967 -0.997355,5.331457 l -0.131257,0.973585 0.911055,0.225824 c 2.647799,0.656543 3.537151,0.92333 4.865852,1.45986 0.80137,0.323583 1.550535,0.567854 1.665015,0.542602 0.11448,-0.02524 0.465997,-1.536988 0.781346,-3.359483 1.552062,-8.969976 1.420651,-8.410268 2.012281,-8.55245 1.16069,-0.278947 3.2742,-1.473287 4.358391,-2.4629 3.704989,-3.381751 4.61156,-8.618035 2.267561,-13.096356 -0.86038,-1.643785 -3.0377,-3.801761 -4.599718,-4.558896 -1.426929,-0.691647 -2.944627,-1.056793 -4.451408,-1.11621 z m -27.115658,0.06253 c -1.853499,0.0066 -2.399186,0.09148 -3.592545,0.556037 -1.776499,0.691555 -2.939674,1.490607 -4.233333,2.90835 -1.939102,2.125078 -2.789494,4.367665 -2.789494,7.356655 0,2.12751 0.541991,3.97087 1.659329,5.644102 1.531501,2.293438 3.657124,3.949724 5.948474,4.634857 l 0.842843,0.251667 0.360701,2.113047 c 0.683681,4.00508 1.596287,9.514157 1.596287,9.636622 0,0.153863 0.280961,0.06373 2.601391,-0.832508 1.03033,-0.397942 2.561188,-0.865222 3.401859,-1.038178 0.840669,-0.172966 1.58106,-0.366665 1.644862,-0.430466 0.138798,-0.138795 -0.09666,-1.521111 -1.081072,-6.349482 -0.40649,-1.99372 -0.738973,-3.823298 -0.738973,-4.065902 0,-0.270629 0.24434,-0.585637 0.632518,-0.814938 0.991741,-0.58583 2.634369,-2.411619 3.301609,-3.669543 3.21256,-6.056471 0.218561,-13.536525 -6.223911,-15.549954 -0.787148,-0.246002 -1.827815,-0.355867 -3.330545,-0.350366 z M 118.38356,78.4396 c -5.4613,0.01087 -10.83084,0.334656 -13.17129,0.794269 -8.3241,1.63468 -14.830472,6.177298 -19.081003,13.322183 -1.41655,2.381134 -1.443389,2.220246 0.62787,3.757909 l 1.796788,1.334286 0.325561,-0.45992 c 1.885112,-2.661536 3.461306,-4.224727 5.737635,-5.691124 2.05182,-1.321774 3.213278,-1.874636 5.775359,-2.749188 3.25365,-1.110612 5.79187,-1.558432 9.71052,-1.713073 l 3.41323,-0.134358 -0.14677,0.605131 c -0.22712,0.937305 -2.24839,4.87313 -3.37498,6.571691 -1.63554,2.465938 -5.22697,6.010124 -7.89099,7.787124 -2.122833,1.416 -5.099155,2.95806 -7.044536,3.65042 -0.443719,0.15792 -0.847719,0.39385 -0.89762,0.524 -0.04994,0.13015 0.257749,0.78161 0.683681,1.44746 0.910849,1.42388 2.374014,4.47857 2.374014,4.9568 0,0.48392 0.347564,0.42554 2.910931,-0.48886 5.55972,-1.98327 9.67439,-4.66415 13.96091,-9.09608 5.48334,-5.669343 8.59492,-12.340298 9.12761,-19.569868 0.22696,-3.0802 0.13785,-3.67811 -0.64544,-4.337203 -0.60562,-0.509278 -0.68029,-0.518698 -4.19148,-0.511599 z M 13.114536,78.4422 c -2.603428,-0.0031 -4.6648949,0.107204 -5.1201,0.360185 -0.8002799,0.44476 -0.9499785,1.401871 -0.7027994,4.497398 0.6171512,7.728879 3.7952414,14.125371 10.2019604,20.534657 4.00355,4.00515 7.663291,6.26847 13.744381,8.50026 1.41078,0.51776 2.081528,0.55543 2.081528,0.11627 0,-0.70954 2.621841,-5.7704 3.188951,-6.1557 0.107791,-0.0732 -1.432293,-0.92853 -3.422012,-1.90065 -1.98973,-0.97214 -4.452244,-2.36424 -5.472536,-3.09336 -4.48011,-3.201484 -8.269856,-8.088862 -10.335286,-13.328893 l -0.421161,-1.068152 3.391524,0.121441 c 3.97215,0.142071 6.170742,0.498425 9.203571,1.491898 5.26909,1.726026 8.813702,4.21721 12.006503,8.438761 l 0.541568,0.716235 1.643827,-1.233 c 0.903991,-0.678215 1.723361,-1.297845 1.820561,-1.377175 C 45.720137,94.85418 43.916309,91.48356 42.636249,89.776392 38.612699,84.410397 32.687916,80.732826 25.718934,79.275271 23.297647,78.768866 17.453586,78.44745 13.114536,78.442246 Z m 15.966469,14.394471 c -0.240599,0.01217 -0.409009,0.09103 -0.642339,0.243911 -0.684239,0.448302 -0.669992,0.916382 0.05788,1.920296 1.432581,1.975911 4.902044,4.72104 8.481653,6.710702 l 5.29e-4,5.3e-4 c 2.026822,1.12657 2.404464,1.22958 2.791044,0.76377 0.38286,-0.4613 0.149397,-1.05952 -1.234032,-3.159489 -2.11764,-3.21442 -5.35546,-5.657353 -8.379849,-6.322611 -0.521496,-0.11471 -0.834271,-0.169264 -1.07487,-0.157096 z m 72.257085,0.06666 c -2.007316,-0.07737 -6.279721,2.151388 -8.339026,4.827611 -1.7316,2.250345 -2.762355,4.453738 -2.300118,4.915978 0.226381,0.22637 0.435708,0.16996 1.876888,-0.50384 4.448961,-2.08007 9.848496,-6.643935 9.848496,-8.324561 0,-0.607115 -0.41713,-0.889397 -1.08624,-0.915188 z M 49.74331,103.49912 c -0.933826,-0.0436 -1.92105,0.0965 -2.925406,0.43873 -2.125009,0.72406 -3.923789,3.21232 -3.923789,5.42809 0,1.39971 0.343702,2.15024 1.139981,2.4908 1.112409,0.47575 1.968738,0.0897 2.616377,-1.17977 0.769692,-1.50877 2.116376,-2.10535 3.627686,-1.60714 0.644861,0.21257 1.015265,0.548 1.677935,1.52136 0.81571,1.19815 0.889728,1.25178 1.825728,1.31258 0.83435,0.0542 1.050367,-0.0176 1.491898,-0.49403 0.70113,-0.75653 0.840335,-1.69857 0.449586,-3.04271 -0.858174,-2.95199 -3.178522,-4.7372 -5.979996,-4.86791 z m 31.345888,0.0336 c -1.998729,-10e-4 -3.128489,0.43782 -4.595069,1.78439 -1.913938,1.75731 -2.589551,4.59993 -1.422652,5.98672 0.448681,0.53322 0.689603,0.64027 1.438672,0.64027 0.95336,0 1.4404,-0.39929 2.205551,-1.80816 0.47189,-0.86889 1.665536,-1.38929 2.797244,-1.21957 0.692282,0.10382 1.071491,0.32463 1.643311,0.95757 0.40876,0.45245 0.743109,0.91246 0.743109,1.02216 0,0.4401 0.936194,1.048 1.613852,1.048 0.921211,0 1.433375,-0.29189 1.818495,-1.03663 1.032631,-1.9969 -0.717997,-5.51018 -3.387908,-6.79959 -0.993378,-0.47974 -1.463786,-0.57453 -2.854605,-0.57516 z m -46.619893,13.43433 c -0.276352,-0.005 -0.555669,-0.002 -0.837676,0.0108 l -1.456756,0.0662 -0.462505,2.28927 c -1.563571,7.74074 -0.288655,15.69759 3.656624,22.81928 1.614911,2.91509 3.537109,5.39884 6.188771,7.99692 l 2.347142,2.29961 0.07855,25.41756 c 0.04639,15.07505 0.04993,21.36444 0.223242,24.0683 a 94.908057,94.908057 0 0 0 13.3072,2.02727 c 0.909749,-0.20503 1.446369,-0.55108 2.273763,-1.15704 0.712568,-0.52186 1.154285,-1.08547 1.665013,-2.12493 0.615881,-1.25334 0.694848,-1.60596 0.688848,-3.08508 -0.0058,-1.514 -0.07562,-1.79549 -0.745175,-3.01946 -1.327029,-2.42583 -3.701135,-3.71478 -6.382555,-3.46439 -0.746612,0.0697 -1.72126,0.31661 -2.16576,0.54829 -0.4445,0.23169 -0.852188,0.42116 -0.905888,0.42116 -0.144979,0 -0.118406,-33.42349 0.02687,-33.80207 0.111339,-0.2901 0.187372,-0.29124 0.727604,-0.0119 1.222769,0.63232 3.937537,1.53268 6.040456,2.0035 1.890281,0.42318 2.701491,0.48345 6.45542,0.47646 l 4.293277,-0.008 0.109038,1.98541 c 0.133612,2.42774 0.782759,4.41779 2.16421,6.63422 2.340951,3.75588 5.918588,5.86269 9.954947,5.86269 2.029489,0 3.586999,-0.42135 4.278808,-1.15755 l 0.508497,-0.54106 0.05736,-10.52028 0.05788,-10.52081 2.664436,-2.62826 c 2.899529,-2.86015 4.992249,-5.73789 6.754109,-9.28832 2.233512,-4.50088 3.462589,-9.69841 3.47059,-14.67456 0.0027,-1.74465 -0.522878,-6.0518 -0.990637,-8.11785 l -0.165365,-0.72864 -1.507918,-0.061 c -4.783659,-0.19443 -8.955752,2.39301 -11.072191,6.86677 -0.34141,0.72168 -0.621051,1.4125 -0.621152,1.53531 -9e-5,0.12281 0.290055,0.82187 0.644406,1.55339 1.31799,2.72084 1.874245,6.34028 1.473293,9.58443 -0.85606,6.92645 -6.157921,13.45791 -13.3227,16.41243 -5.78241,2.38449 -13.149373,2.18239 -18.597313,-0.51004 -5.78163,-2.85738 -10.019414,-7.77561 -11.655102,-13.52579 -0.39125,-1.37536 -0.473469,-2.1722 -0.47284,-4.57905 0.0011,-3.31386 0.273185,-4.64187 1.481045,-7.22437 0.848381,-1.81388 0.850151,-1.79921 -0.490408,-4.18217 -2.108401,-3.74786 -5.594162,-5.86426 -9.739458,-5.94641 z m 22.902995,6.65023 c -0.528376,0.01 -0.864489,0.1385 -1.458825,0.4868 -1.054891,0.61821 -1.520793,1.3503 -1.697054,2.66702 -0.22647,1.69181 1.400609,3.8858 3.35225,4.52014 0.898808,0.29214 2.352969,0.12191 2.989998,-0.34985 1.612371,-1.19404 1.880637,-2.73634 0.838708,-4.82141 -0.809191,-1.61935 -1.807162,-2.34294 -3.415813,-2.47634 -0.235627,-0.0195 -0.433139,-0.0296 -0.609264,-0.0263 z m 15.156698,0.0165 c -2.285825,0.0741 -4.032313,2.12408 -4.032313,4.85035 0,1.20002 0.444309,2.00284 1.436089,2.59416 1.746571,1.04132 4.770173,-0.17744 5.900415,-2.37815 1.173038,-2.28404 -0.263427,-4.8408 -2.840138,-5.05447 -0.156821,-0.013 -0.311666,-0.0168 -0.464053,-0.0119 z m 2.483051,11.21586 c -0.656468,0.008 -1.287205,0.58041 -1.85725,1.70532 -1.291241,2.54818 -4.130101,4.55892 -6.84661,4.84983 -3.611099,0.38671 -6.800913,-1.28571 -8.724532,-4.57492 -0.447072,-0.76445 -1.005451,-1.52501 -1.240753,-1.68981 -0.662358,-0.46394 -1.535462,-0.36725 -2.139402,0.23668 -0.73946,0.73944 -0.592905,1.90298 0.460436,3.65765 1.62333,2.70418 4.279106,4.76062 7.243487,5.60896 0.767919,0.21977 2.033558,0.37237 3.158979,0.38137 4.895789,0.0387 8.966806,-2.44771 11.117668,-6.79028 0.79294,-1.60101 0.797332,-2.0185 0.02997,-2.78588 -0.404895,-0.40489 -0.808112,-0.60352 -1.201995,-0.59892 z m -14.836303,37.70202 c -0.143047,-2e-5 -0.287245,0.007 -0.432015,0.0196 -2.50707,0.22733 -4.195265,1.92264 -4.503084,4.52169 -0.23368,1.97301 0.986602,4.00884 2.951242,4.92372 1.691079,0.78749 3.376216,0.56644 4.973855,-0.65215 1.644422,-1.25422 2.281523,-3.1966 1.702223,-5.18935 -0.630029,-2.1672 -2.546504,-3.62311 -4.692221,-3.62355 z m 20.363097,10.79986 c -0.807357,0.0171 -1.6291,0.11477 -2.417939,0.30076 -4.74466,1.11867 -8.179962,5.16078 -8.52351,10.02885 -0.161139,2.28329 0.287782,3.95948 1.59835,5.96707 1.30869,2.00471 2.928605,3.3361 4.902544,4.02353 a 94.908057,94.908057 0 0 0 10.506853,-1.80816 v -7.47654 c 0,-8.51825 -0.0089,-8.71429 -0.435634,-9.2568 -0.91263,-1.16021 -3.208586,-1.83016 -5.630664,-1.77871 z"
    />
    <path
    id="circle-path"
    fill="none"
    stroke="#000000"
    stroke-width="1"
    stroke-miterlimit="4"
    stroke-dasharray="none"
    stroke-opacity="1"
    d="M 160.26051,109.39293 A 95.004552,95.004607 0 0 0 65.25596,14.388319 95.004552,95.004607 0 0 0 -29.74859,109.39293 95.004552,95.004607 0 0 0 65.25596,204.39752 95.004552,95.004607 0 0 0 160.26051,109.39293 Z"
    />
    <text
    font-size="17"
    text-anchor="middle"
    direction="ltr"
    dy="20"
    pointer-events="none"
    style="user-select: none"
    >
    <textPath href="#circle-path" startOffset="70%">
    ${this.i18nManager.getTranslation('color_giraffe')}
    </textPath>
    </text>
    </g>
    </g>
    </svg>
    </div>

    <div id="giraffe-details">
    <div>ID: ${this.fid}</div>
    <div>X: ${this.coordinates[0]?.toFixed(2)}</div>
    <div>Y: ${this.coordinates[1]?.toFixed(2)}</div>
    <div>Color: ${this.color}</div>
    </div>

    <div class="btn-group-flex">
    <button id="zoom-btn" class="gg-button" title="" i18n="zoom_on_giraffe">
    Zoom to
    </button>
    <button id="delete-btn" class="gg-button" title="" i18n="delete">
    Delete
    </button>
    </div>
    </div>
  5. In helloworld/hellodetails/style.css, copy the following code:

    #hello-details {
    background: #f2f2f2;
    padding: 1.5rem;
    margin: 10px 0px;
    }

    #giraffe-avatar {
    width: 100%;
    height: 100%;
    max-width: 180px;
    }

    #avatar-group {
    cursor: pointer;
    }

    #avatar-group:hover {
    stroke: #2f4355;
    }

    #giraffe-details {
    margin: 25px 0;
    }

    .centered {
    display: flex;
    align-items: center;
    justify-content: center;
    }

    .btn-group-flex {
    display: flex;
    flex-direction: row;
    gap: 0.5rem;
    margin: 0.5rem auto;
    }

    .btn-group-flex > button,
    .btn-group-flex > select,
    .btn-group-flex > input {
    flex: 50%;
    }
  6. Modify helloworld/template.html, as following to render the details box in the side panel (note we've removed the stringified list of markers):

    <div id="panel">
    <div id="content">
    <h1 i18n="hello">Hello World!</h1>

    <!-- Button group -->
    <div class="btn-group-flex">
    <!-- Draw button -->
    <button
    id="draw-btn"
    class="${this.helloworldState.draw ? 'gg-button active' : 'gg-button'}"
    title="${this.i18nManager.getTranslation('draw_giraffe_help')}"
    onclick="${() => this.setDraw(!this.helloworldState.draw)}"
    i18n="draw_giraffe"
    >
    Draw
    </button>

    <!-- Delete all button -->
    <button
    id="delete-points-btn"
    class="gg-button"
    title="${this.i18nManager.getTranslation('delete_all_giraffes_help')}"
    onclick="${() => this.deleteAllFeatures()}"
    i18n="delete_all_giraffes"
    >
    Delete
    </button>
    </div>

    <!-- Details box -->
    <girafe-hello-details
    id="girafe-details"
    element=""
    ></girafe-hello-details>

    <!-- List of chips -->
    <div class="chip-container">
    ${ this.helloworldState.markers.map(f => uHtmlFor(f,
    f.id)`<girafe-hello-chip
    fid="${f.id}"
    color="${f.color}"
    ></girafe-hello-chip
    >`) }
    </div>
    </div>
    </div>
  7. In src/main.ts:

    Import the HelloDetailsComponent by adding the following line to the list of imports at the beginning of the file:

    import HelloDetailsComponent from "./components/helloworld/hellodetails/component";

    Define the component's name (alias) by adding the following line to the list of definitions on the customElements variable:

    customElements.define("girafe-hello-details", HelloDetailsComponent);

Look in your browser and open the HelloWorld panel. When you draw new markers, modify them or delete them (with the delete button), in addition to the colored chips, you should now see an information box displaying details about the selected marker. If you click on the giraffe avatar, it will change the marker's color. You can also delete the marker or zoom to it, if you click on the dedicated buttons. Congratulations! You have successfully finished this tutorial!

Congratulations