Skip to main content

Add 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 custom component’s initial structure
  4. Adding markers on the map
  5. Adding select, draw and modify interactions to the map markers
  6. Using the extended state manager to store the component's state
  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 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;
    lidarPanelVisible: 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,
    lidarPanelVisible: 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 public/icons/ folder

  2. Open index.html

  3. In index.html, at the end of the <girafe-menu-button> element, add the following <button> element:

    ...
    <img alt="help-icon" src="icons/info.svg" />
    <span>About</span>
    </button>
    <button slot="menu-content" class="girafe-button-medium" onclick="document.geogirafe.state.interface.helloPanelVisible = true">
    <img alt="helloworld-icon" src="icons/helloworld.svg" />
    <span>Hello</span>
    </button>
    </girafe-menu-button>
  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/) and in the Geogirafe app click on the top right doted icon to open the lateral menu 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 custom 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)
  3. In helloworld/component.ts at the beginning, import the following definitions:

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

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

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

    constructor() {
    super("hello");
    }

    // 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.stateManager.subscribe(
    "interface.darkFrontendMode",
    (_oldValue: boolean, newValue: boolean) => {
    this.darkFrontendMode = newValue;
    }
    )
    );
    }

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

    // Register visibility events
    registerVisibilityEvents(): void {
    // Hello World panel toggle
    this.stateManager.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>Hello World!</h1>
    </div>
    </div>
  5. In style.css, add the following styling rules:

    #panel {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    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;
    }

    #content button {
    background-color: var(--bkg-color);
    color: var(--text-color);
    border-radius: 3px;
    outline: 0;
    border: solid 1px var(--text-color);
    cursor: pointer;
    }

    #content button:hover {
    background-color: var(--bkg-color-grad2);
    }

    .btn-group {
    margin: 5px 0;
    }
  6. 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);
  7. Finally, in index.html, add the girafe-helloworld custom element you just defined, in the right <girafe-lr-panel> panel element:

    <girafe-lr-panel class="panel-right" dock="right">
    <girafe-drawing
    slot="main"
    data-toggle-path="interface.printPanelVisible"
    ></girafe-drawing>
    <girafe-print
    slot="main"
    data-toggle-path="interface.drawingPanelVisible"
    ></girafe-print>
    <girafe-lidar-panel
    slot="main"
    data-toggle-path="interface.lidarPanelVisible"
    ></girafe-lidar-panel>
    <girafe-helloworld
    slot="main"
    data-toggle-path="interface.helloPanelVisible"
    ></girafe-helloworld>
    </girafe-lr-panel>

If you look in your browser and click on the Hello button, you should now see a side panel with a Hello World! title.

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 GirafeHTMLElement from "../../base/GirafeHTMLElement";
    import type { Callback } from "../../tools/state/statemanager";
    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;
    private readonly map: Map;
    private helloSource: VectorSource | null = null;
    private helloLayer: VectorLayer<VectorSource> | null = null;
    • In the constructor, define the map, vector source (helloSource) and vector layer (helloLayer) properties:
     constructor() {
    super('hello');

    // 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 })
    ],
    wrapX: false
    });

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

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

    }
    • Outside the constructor, add the following method 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 add the HelloLayer to the map:
    // Renders the component
    private renderComponent() {
    super.render();
    super.girafeTranslate();
    this.activateTooltips(false, [800, 0], 'top-end');

    // 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 blue points on the map. If you close the panel, the points 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 GirafeHTMLElement from "../../base/GirafeHTMLElement";
    import type { Callback } from "../../tools/state/statemanager";
    import { v4 as uuidv4 } from "uuid";
    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 { 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';
    styleUrl = './style.css';

    private readonly eventsCallbacks: Callback[] = [];
    darkFrontendMode: boolean = false;
    visible: boolean = false;
    private readonly map: Map;
    private helloSource: VectorSource | null = null;
    private helloLayer: VectorLayer<VectorSource> | null = null;
    selectInteraction: Select;
    drawInteraction: Draw;
    modifyInteraction: Modify;
    draw: boolean = false;
    • In the constructor, initialize the draw mode to false:
     constructor() {
    super('hello');

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

    // Initialize the drawing mode to off (false)
    this.draw = 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: [],
    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 = this.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 and modification interactions to the map:
    // Add interactions to the map
    this.map.addInteraction(this.selectInteraction);
    this.map.addInteraction(this.modifyInteraction);
    • In renderEmptyComponent(), add calls to remove interactions :
    // Hide the side panel, and removes listeners and interactions
    private renderEmptyComponent() {
    this.state.selection.enabled = true;

    // Unregister events
    this.unregisterEvents();

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

    // Remove map interactions
    this.map.removeInteraction(this.drawInteraction);
    this.map.removeInteraction(this.modifyInteraction);

    // Render empty component
    this.renderEmpty();
    }
    • Outside the constructor, add the following methods to toggle/set the drawing mode:
    // Toggle drawing mode
    toggleDraw(): void {
    this.draw = !this.draw;
    this.setDraw(this.draw);
    }

    // Set drawing mode
    setDraw(val: boolean): void {
    this.draw = val;
    const target = this.shadow.getElementById('draw-btn')!;
    if (val) {
    target.innerHTML = 'Draw (ON)';
    this.map.addInteraction(this.drawInteraction);
    this.map.addInteraction(this.modifyInteraction);
    } else {
    target.innerHTML = 'Draw (OFF)';
    this.map.removeInteraction(this.drawInteraction);
    }
    }
    • Outside the constructor, add the following method to delete all markers:
    // Delete all features
    deleteAllFeatures(): void {
    this.helloSource?.clear();
    }
    • Outside the constructor, add the following methods to generate random colors (which will be used to color the newly drawn map markers):
    // Random color generator function
    randomColor(): string {
    const letters = '0123456789ABCDEF';
    let color = '#';
    for (let i = 0; i < 6; i++) {
    color += letters[Math.floor(Math.random() * 16)];
    }
    return color;
    }
  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:

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

    <!-- Buttons -->
    <div class="btn-group">
    <button
    id="draw-btn"
    class=""
    title="Draw"
    onclick="${() => this.toggleDraw()}"
    >
    Draw (OFF)
    </button>
    <button
    id="delete-points-btn"
    class=""
    title="Delete all points"
    onclick="${() => this.deleteAllFeatures()}"
    >
    Delete all
    </button>
    </div>
    </div>
    </div>

Look in your browser and open the HelloWorld panel, you should now see a draw button. If you click on it, 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 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.

Using the extended state manager to store the component's state

In this step, you will learn how to use the extented state manager to store the helloWorld component's list of markers and the draw mode. The result will look like this:

Hello World side panel

Since our HelloWorldComponent extends the <GirafeHTMLElement> class, it automatically inherits access to Geogirafe's global state manager (via this.state). The state of core components is directly accessible on this.state (for example: this.state.themes, state.basemaps, state.mouseCoordinates, etc). However, the state of custom components must be stored in the extended state manager via this.state.extendedState (for example, we will use this.state.extendedState.helloworld here).

We will start by definining a typescript interface to represent the map markers and a classes to represent the state of the helloWorld component:

  1. In the helloWorld component's root folder, create new file called utils.ts with the following classes:

    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 imports for the Marker and HelloState definitions you just created and for the OpenLayers Point and VectorSourceEvent classes:

    import GirafeHTMLElement from "../../base/GirafeHTMLElement";
    import type { Callback } from "../../tools/state/statemanager";
    import { v4 as uuidv4 } from "uuid";
    import MapManager from "../../tools/state/mapManager";
    import Map from "ol/Map";
    import VectorSource, { 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 { Geometry, Point } 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 { HelloState, Marker } from "./utils";
  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';
    styleUrl = './style.css';

    private readonly eventsCallbacks: Callback[] = [];
    darkFrontendMode: boolean = false;
    visible: boolean = false;
    private readonly map: Map;
    private helloSource: VectorSource | null = null;
    private helloLayer: VectorLayer<VectorSource> | 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: [],
    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:

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

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

    <!-- Buttons -->
    <div class="btn-group">
    <button
    id="draw-btn"
    class=""
    title="Draw"
    onclick="${() => this.toggleDraw()}"
    >
    Draw (OFF)
    </button>
    <button
    id="delete-points-btn"
    class=""
    title="Delete all points"
    onclick="${() => this.deleteAllFeatures()}"
    >
    Delete all
    </button>
    </div>
    </div>
    </div>
  6. In the toggleDraw() and setDraw() methods, replace all calls to this.draw by this.helloworldState.draw:

    // Toggle drawing mode
    toggleDraw(): void {
    this.helloworldState.draw = !this.helloworldState.draw;
    this.setDraw(this.helloworldState.draw);
    }

    // Set drawing mode
    setDraw(val: boolean): void {
    this.helloworldState.draw = val;
    const target = this.shadow.getElementById('draw-btn')!;
    if (val) {
    target.innerHTML = 'Draw (ON)';
    this.map.addInteraction(this.drawInteraction);
    this.map.addInteraction(this.modifyInteraction);
    } else {
    target.innerHTML = 'Draw (OFF)';
    this.map.removeInteraction(this.drawInteraction);
    }
    }

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

For the first time, we will use the subscribe method from Geogirafe's state manager. The subscribe method takes as input a state manager variable's path (string or regular expresion) and a callback function (whith old property values, new property value and optionally the parent variable as input arguments):

 subscribe(path: string | RegExp, callback: Callback): Callback

More explicitely, the following syntax is used:

this.stateManager.subscribe(
"path 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 on all the marker properties (selected, color, etc) in the hellowWorld's component state manager (this.state.extendedState.helloworld), we use:

// Note that we use a regular expression with wildcards (*) here
this.stateManager.subscribe(
/extendedState\.helloworld\.markers\..*\..*/,
(_oldPropVal: any, _newPropVal: any, _parent: Marker) => {
// callback function executed when a change on the observed variable is detected
}
);

If we wanted to monitor change only on a specific property of the markers in the list, for example to monitor changes on the "selected" property of the markers, we can use:

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

To observe changes on the list of markers (i.e. change of the array length, when a marker is added or deleted) in the hellowWorld's component state manager (this.state.extendedState.helloworld), we use:

// Note that we use a string containing the path to the marker property here
this.stateManager.subscribe(
"extendedState.helloworld.markers",
(_oldPropVal: Marker[], _newPropVal: Marker[], _parent: HelloState) => {
// callback function exectuted when a change on the observed variable is detected
}
);
  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 GirafeHTMLElement from "../../../base/GirafeHTMLElement";
    import type { Callback } from "../../../tools/state/statemanager";
    import { HelloState, Marker } from "../utils";

    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;
    }

    // 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();
    }
    }
    )
    );

    // Click on chip event
    this.addEventListener("click", (_e: Event) => {
    console.debug(`click 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;
    }
    }
    });
    }

    /*
    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}"></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>Hello World!</h1>

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

    <!-- Buttons -->
    <div class="btn-group">
    <button
    id="draw-btn"
    class=""
    title="Draw"
    onclick="${() => this.toggleDraw()}"
    >
    Draw (OFF)
    </button>
    <button
    id="delete-points-btn"
    class=""
    title="Delete all points"
    onclick="${() => this.deleteAllFeatures()}"
    >
    Delete all
    </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 (with the delete button), you should see a list of colored chips being 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 GirafeHTMLElement from "../../../base/GirafeHTMLElement";
    import MapManager from "../../../tools/state/mapManager";
    import type { Callback } from "../../../tools/state/statemanager";
    import Map from "ol/Map";
    import { Feature } from "ol";
    import { Geometry, Point } from "ol/geom";
    import { HelloState, Marker } from "../utils";
    import VectorLayer from "ol/layer/Vector";
    import VectorSource from "ol/source/Vector";
    import BaseLayer from "ol/layer/Base";

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

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

    fid: string = "";
    color: string = "";
    coordinates: number[] = [];
    colorButton: HTMLButtonElement;
    zoomButton: HTMLButtonElement;
    deleteButton: HTMLButtonElement;
    helloworldState: HelloState;
    layers: BaseLayer[];
    helloLayer: VectorLayer<VectorSource> | null;
    helloSource: VectorSource<Feature<Geometry>> | null;

    constructor() {
    super("hellodetails");

    // Import the open layers map instance from Geogirafe's map manager
    this.map = MapManager.getInstance().getMap();
    this.layers = this.map.getLayers().getArray();
    this.helloLayer = null;
    this.helloSource = null;
    this.colorButton = this.shadow.getElementById(
    "color-btn"
    )! as HTMLButtonElement;
    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 helloLayer = this.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 {
    this.layers = this.map.getLayers().getArray();
    this.helloLayer = this.layers.find(
    (layer) => layer.get("name") == "helloworldLayer"
    ) as VectorLayer<VectorSource>;
    this.helloSource = this.helloLayer.getSource() as VectorSource<
    Feature<Geometry>
    >;

    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(
    "color-btn"
    )! as HTMLButtonElement;
    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("change", (_e) => {
    const color = (<HTMLInputElement>_e.target)!.value;
    const target = this.helloworldState.markers.find((x) => x.selected);
    if (color && target) {
    target.color = color;
    }
    });

    // 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.registerEvents();
    this.render();
    super.girafeTranslate();
    this.registerEvents();
    });
    }
    }

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

    <div id="hello-details">
    <label for="color-btn" class="centered" title="Click to change my color!">
    <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">
    <textPath href="#circle-path" startOffset="75%">
    Click to change my color!
    </textPath>
    </text>
    </g>
    </g>
    </svg>
    </label>

    <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>
    <button title="Zoom to this giraffe" id="zoom-btn">Zoom to</button>
    <button title="Delete this giraffe" id="delete-btn">Delete</button>
    </div>
    <input
    type="color"
    title="Change color"
    id="color-btn"
    value="${this.color}"
    style="display: none"
    />
    </div>
  5. In helloworld/hellodetails/style.css, copy the following code:

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

    #hello-details button {
    background-color: var(--bkg-color);
    color: var(--text-color);
    border-radius: 3px;
    outline: 0;
    border: solid 1px var(--text-color);
    cursor: pointer;
    }

    #hello-details button:hover {
    background-color: var(--bkg-color-grad2);
    }

    #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;
    }
  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>Hello World!</h1>

    <!-- Buttons -->
    <div class="control-buttons">
    <button
    id="draw-btn"
    class=""
    title="Draw"
    onclick="${() => this.toggleDraw()}"
    >
    Draw (OFF)
    </button>
    <button
    id="delete-points-btn"
    class=""
    title="Delete all points"
    onclick="${() => this.deleteAllFeatures()}"
    >
    Delete all
    </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, you can 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