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:
- Setting up the project
- Adding a button to the lateral menu bar
- Creating the component’s initial structure
- Adding markers on the map
- Adding select, draw and modify interactions to the map markers
- Using the state manager
- Creating a subcomponent in the side panel to display a list of chips
- 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:
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
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:
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:
-
Open
src/tools/state/state.ts
-
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 calledhelloPanelVisible
:type GraphicalInterface = {
helpVisible: boolean;
drawingPanelVisible: boolean;
printPanelVisible: boolean;
crossSectionPanelVisible: boolean;
helloPanelVisible: boolean;
selectionComponentVisible: boolean;
selectionComponent: string;
aboutVisible: boolean;
shareVisible: boolean;
darkMapMode: boolean;
darkFrontendMode: boolean;
}; -
Further down in the code, in the
State
class, locate theinterface
property, add an entry calledhelloPanelVisible
and set its value tofalse
.// 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:
-
Copy the
helloworld.svg
file to the project'ssrc/assets/icons/
folder -
Open
index.html
in the root directory -
In
index.html
, add the following<button>
element in thebuttonbar
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> -
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:
-
Add a new
helloworld/
folder insrc/components/
-
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.
-
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; -
In
template.html
, add the following HTML:<div id="panel">
<div id="content">
<h1 i18n="hello">Hello Giraffe!</h1>
</div>
</div>i18n translationsGeogirafe 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 thesrc/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 agetTranslation()
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.
-
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"
}
} -
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"
}, -
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%;
} -
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);
-
In
index.html
, add thegirafe-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 attributeThe
data-title
attribute in custom elements is a reference to a variable in the i18n translation files (in our example, thehello-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):
-
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"; -
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 theHelloLayer
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 theHelloLayer
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:
-
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"; -
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
anddrawend
) 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
andmodifyend
) 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 thehelloworld/
directory and add the followingrandomColor()
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 therandomColor()
function fromutils.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"; -
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 syntaxGeogirafe 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:
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 insrc/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:
-
In the
helloWorld
component's root folder, create a new file calledhellostate.ts
:export class Marker {
id: string = "";
coordinates: number[] = [];
color: string = "";
selected: boolean = false;
}
export class HelloState {
draw: boolean = false;
markers: Marker[] = [];
} -
In
helloworld/component.ts
, add an import for the OpenLayersVectorSourceEvent
classe and for theHelloState
andMarker
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"; -
Then, modify the
HelloWorldComponent
class:Add a
helloworldState
property of typeHelloState
to the property definitions at the beggining ofHelloWorldComponent
and remove thedraw
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; -
In the constructor, after the initialization of
helloSource
, add the following event listeners onhelloSource
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();
}); -
In the template (
helloworld/template.html
) of the helloWorld component, add a stringified list of the markers stored in the extended state manager and replacethis.draw
bythis.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> -
In the
setDraw()
method, replace all calls tothis.draw
bythis.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:
-
Copy the
giraffe.svg
file to the project'spublic/icons/
folder -
Add a new
hellochip/
folder insrc/components/helloworld
-
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)
-
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; -
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>
-
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;
} -
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> -
In
helloworld/component.ts
, add the following subscriptions to theeventCallbacks
array in theregisterEvents()
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();
}
)
);
} -
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:
-
Add a new
hellodetails/
folder insrc/components/helloworld
-
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)
-
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; -
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> -
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%;
} -
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> -
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!