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:
- Setting up the project
- Adding a button to the lateral menu bar
- Creating the custom component’s initial structure
- Adding markers on the map
- Adding select, draw and modify interactions to the map markers
- Using the extended state manager to store the component's state
- 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
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;
lidarPanelVisible: 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,
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:
-
Copy the
helloworld.svg
file to the project'spublic/icons/
folder -
Open
index.html
-
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> -
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:
-
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)
-
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; -
In
template.html
, add the following HTML:<div id="panel">
<div id="content">
<h1>Hello World!</h1>
</div>
</div> -
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;
} -
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);
- Import the
-
Finally, in
index.html
, add thegirafe-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):
-
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"; -
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 theHelloLayer
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 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 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:
-
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"; -
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
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 = 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
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 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;
} - Add the typed properties definitions for the interactions (
-
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:
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:
-
In the
helloWorld
component's root folder, create new file calledutils.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[] = [];
} -
In
helloworld/component.ts
, add imports for theMarker
andHelloState
definitions you just created and for the OpenLayersPoint
andVectorSourceEvent
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"; -
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';
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; - Add a
-
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: [],
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:<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> -
In the
toggleDraw()
andsetDraw()
methods, replace all calls tothis.draw
bythis.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:
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
}
);
-
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 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; -
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>
-
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>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> -
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);
- Import the
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:
-
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 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; -
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> -
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;
} -
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> -
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);
- Import the
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!