Play with Labels
Display the Map with floor interactions
Follow Learn the Basics - Floor controls
Label Edition
selection
First of all let's create the label selection / unselect functions:
export default class App extends React.Component {
// ...
currentLabelEdit = null;
async edit(labelObject) {
if (this.currentLabelEdit === labelObject) {
return;
}
if (this.currentLabelEdit !== null) {
await this.currentLabelEdit.unselect();
this.currentLabelEdit = null;
}
if (labelObject !== null) {
await labelObject.select();
}
this.currentLabelEdit = labelObject;
this.setState({edit: this.currentLabelEdit !== null});
}
}
Then let's create a function that will retrieve the label given a mouse click event:
export default class App extends React.Component {
// ...
async getClickedLabel(intersects) {
if (intersects.length === 0) {
return null;
}
// Let's grab the first intersect
let { object } = intersects[0];
if (object.isLabel) {
return object;
}
// If building or space, then let's select the first label if any
if (object.isBuilding || object.isSpace) {
const labels = await object.getLabels();
return labels.length === 0 ? null : labels[0];
}
return null;
}
}
Finally let's just register the event listener, inside the start
function:
async start() {
// Init the Map
await this.adsumRnMap.init({
entityManager: this.entityManager,
deviceId: 323,
});
this.adsumRnMap.mouseManager.addEventListener(
MOUSE_EVENTS.click,
async (event) => {
await this.edit(await this.getClickedLabel(event.intersects));
}
);
// Start the rendering
await this.adsumRnMap.start();
this.setState({ready: true});
}
EditComponent
It order to deal with colors we used tinycolor2
and react-native-color
.
yarn add tinycolor2 react-native-color
Let's create the EditComponent
skeleton which will handle the edition:
import React from 'react';
import {View, StyleSheet, Text, TouchableOpacity, Switch, TextInput, ScrollView, Picker, KeyboardAvoidingView, Platform} from 'react-native';
import {LABEL_ORIENTATION_MODES} from '@adactive/adsum-react-native-map';
import tinycolor from 'tinycolor2';
import {SlidersColorPicker} from 'react-native-color';
const styles = StyleSheet.create({
inputRow: {
flex: 1,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
label: {
flex: 1,
fontSize: 15,
},
input: {
flex: 1,
minHeight: 40,
alignItems: 'center',
justifyContent: 'center'
},
picker: {
flex: 1,
minHeight: 40,
// alignItems center is buggy on Picker IOS
// alignItems: 'center'
justifyContent: 'center'
},
});
/**
* All the input types
*
* @type {{bool: string, number: string, color: string, select: string, text: string}}
*/
const INPUT_TYPES = {
bool: 'bool',
number: 'number',
color: 'color',
select: 'select',
text: 'text',
};
export default class EditComponent extends React.Component {
state = {
/**
* The form data
*
* @type {object[]}
*/
form: [],
/**
* The color picker data
*
* @type {?{value: string, onChange: function}}
*/
colorPicker: null,
};
componentDidMount() {
// Create the form
this.createForm();
}
UNSAFE_componentWillReceiveProps() {
// Update the form
this.createForm();
}
/**
* Create the form according to labelObject props
*
* @return {Promise<void>}
*/
async createForm() {
// Get some labelObject values
const offset = await this.props.labelObject.getOffset();
const scale = await this.props.labelObject.getScale();
const orientationMode = await this.props.labelObject.getOrientationMode();
const isAutoScale = await this.props.labelObject.isAutoScale();
// Create the form
const form = [
{
name: 'isAutoScale', // Property name
value: isAutoScale, // Property value
type: INPUT_TYPES.bool, // Property type
disabled: false, // We want the input to be displayed
onChange: async (value) => { // On change callback
this.refreshInput('isAutoScale', value); // Update the input
await this.props.labelObject.setAutoScale(value); // Update the label
/**
* Special case for autoScale
* When autoScale is enabled, then you cannot edit anymore the scale manually
* And when it's enabled back, we need to update the scale property because it may has been changed
*/
if (value) {
this.disableInput('scaleX');
this.disableInput('scaleY');
this.disableInput('scaleZ');
} else {
const newScale = await this.props.labelObject.getScale();
this.refreshInput('scaleX', newScale.x);
this.refreshInput('scaleY', newScale.y);
this.refreshInput('scaleZ', newScale.z);
}
}
},
{
name: 'isPermanentDisplay',
value: await this.props.labelObject.isPermanentDisplay(),
type: INPUT_TYPES.bool,
disabled: false,
onChange: async (value) => {
this.refreshInput('isPermanentDisplay', value);
await this.props.labelObject.setPermanentDisplay(value);
}
},
{
name: 'orientationMode',
value: orientationMode,
type: INPUT_TYPES.select,
disabled: false,
choices: [LABEL_ORIENTATION_MODES.BILLBOARD, LABEL_ORIENTATION_MODES.STATIC],
onChange: async (value) => {
this.refreshInput('orientationMode', value);
await this.props.labelObject.setOrientationMode(value);
/**
* Special case rotation is only available for static mode
* Here no need to update the rotation as it hasn't been changed
*/
if (value === LABEL_ORIENTATION_MODES.STATIC) {
this.enableInput('rotation');
} else {
this.disableInput('rotation');
}
}
},
{
name: 'offsetX',
value: offset.x,
type: INPUT_TYPES.number,
disabled: false,
onChange: async (value) => {
this.refreshInput('offsetX', value);
const v = parseFloat(value);
if (!isNaN(v)) {
await this.props.labelObject.moveTo(v, offset.y, offset.z);
}
}
},
{
name: 'offsetY',
value: offset.y,
type: INPUT_TYPES.number,
disabled: false,
onChange: async (value) => {
this.refreshInput('offsetY', value);
const v = parseFloat(value);
if (!isNaN(v)) {
await this.props.labelObject.moveTo(offset.x, v, offset.z);
}
}
},
{
name: 'offsetZ',
value: offset.z,
type: INPUT_TYPES.number,
disabled: false,
onChange: async (value) => {
this.refreshInput('offsetZ', value);
const v = parseFloat(value);
if (!isNaN(v)) {
await this.props.labelObject.moveTo(offset.x, offset.y, v);
}
}
},
{
name: 'opacity',
value: await this.props.labelObject.getOpacity(),
type: INPUT_TYPES.number,
disabled: false,
onChange: async (value) => {
this.refreshInput('opacity', value);
const v = parseFloat(value);
if (!isNaN(v)) {
await this.props.labelObject.setOpacity(v);
}
}
},
{
name: 'scaleX',
value: scale.x,
type: INPUT_TYPES.number,
disabled: isAutoScale, // Disable if it's autoScaling
onChange: async (value) => {
this.refreshInput('scaleX', value);
const v = parseFloat(value);
if (!isNaN(v)) {
await this.props.labelObject.setScale(v, scale.y, scale.z);
}
}
},
{
name: 'scaleY',
value: scale.y,
type: INPUT_TYPES.number,
disabled: isAutoScale, // Disable if it's autoScaling
onChange: async (value) => {
this.refreshInput('scaleY', value);
const v = parseFloat(value);
if (!isNaN(v)) {
await this.props.labelObject.setScale(scale.x, v, scale.z);
}
}
},
{
name: 'scaleZ',
value: scale.z,
type: INPUT_TYPES.number,
disabled: isAutoScale, // Disable if it's autoScaling
onChange: async (value) => {
this.refreshInput('scaleZ', value);
const v = parseFloat(value);
if (!isNaN(v)) {
await this.props.labelObject.setScale(scale.x, scale.y, v);
}
}
},
{
name: 'rotation',
value: await this.props.labelObject.getRotation(),
type: INPUT_TYPES.number,
disabled: orientationMode !== LABEL_ORIENTATION_MODES.STATIC, // Disable if it's STATIC
onChange: async (value) => {
this.refreshInput('rotation', value);
const v = parseFloat(value);
if (!isNaN(v)) {
await this.props.labelObject.setRotation(v);
}
}
},
];
/**
* Properties only for LabelImage
*/
if (this.props.labelObject.isLabelImage) {
form.push({
name: 'height',
value: await this.props.labelObject.getHeight(),
type: INPUT_TYPES.number,
disabled: false,
onChange: async (value) => {
this.refreshInput('height', value);
const v = parseInt(value);
if (!isNaN(v)) {
await this.props.labelObject.setHeight(v);
}
}
});
form.push({
name: 'width',
value: await this.props.labelObject.getWidth(),
type: INPUT_TYPES.number,
disabled: false,
onChange: async (value) => {
this.refreshInput('width', value);
const v = parseInt(value);
if (!isNaN(v)) {
await this.props.labelObject.setWidth(v);
}
}
});
}
/**
* Properties only for LabelText
*/
if (this.props.labelObject.isLabelText) {
form.push({
name: 'text',
value: await this.props.labelObject.getText(),
type: INPUT_TYPES.text,
multiline: true, // text can be multilines
disabled: false,
onChange: async (value) => {
this.refreshInput('text', value);
await this.props.labelObject.setText(value);
}
});
const style = await this.props.labelObject.getStyle();
form.push({
name: 'size',
value: style.size,
type: INPUT_TYPES.number,
disabled: false,
onChange: async (value) => {
this.refreshInput('size', value);
const v = parseFloat(value);
if (!isNaN(v)) {
await this.props.labelObject.setStyle({size: v});
}
}
});
form.push({
name: 'color',
value: style.color,
type: INPUT_TYPES.color,
disabled: false,
onChange: async (value) => {
this.refreshInput('color', value);
await this.props.labelObject.setStyle({color: value});
}
});
form.push({
name: 'lineHeight',
value: style.lineHeight,
type: INPUT_TYPES.number,
disabled: false,
onChange: async (value) => {
this.refreshInput('lineHeight', value);
const v = parseFloat(value);
if (!isNaN(v)) {
await this.props.labelObject.setStyle({lineHeight: v});
}
}
});
form.push({
name: 'hasBackground',
value: style.backgroundColor !== null, // This is not a property but more a derived one when backgroundColor is null
type: INPUT_TYPES.bool,
disabled: false,
onChange: async (value) => {
this.refreshInput('hasBackground', value);
if (value) {
this.refreshInput('backgroundColor', '#cccccc');
this.enableInput('backgroundOpacity');
this.enableInput('backgroundRadius');
await this.props.labelObject.setStyle({backgroundColor: "#cccccc"});
} else {
this.disableInput('backgroundColor');
this.disableInput('backgroundOpacity');
this.disableInput('backgroundRadius');
await this.props.labelObject.setStyle({backgroundColor: null});
}
}
});
form.push({
name: 'backgroundColor',
value: style.backgroundColor,
type: INPUT_TYPES.color,
disabled: style.backgroundColor === null,
onChange: async (value) => {
this.refreshInput('backgroundColor', value);
await this.props.labelObject.setStyle({backgroundColor: value});
}
});
form.push({
name: 'backgroundOpacity',
value: style.backgroundOpacity,
type: INPUT_TYPES.number,
disabled: style.backgroundColor === null,
onChange: async (value) => {
this.refreshInput('backgroundOpacity', value);
const v = parseFloat(value);
if (!isNaN(v)) {
await this.props.labelObject.setStyle({backgroundOpacity: v});
}
}
});
form.push({
name: 'backgroundRadius',
value: style.backgroundRadius,
type: INPUT_TYPES.number,
disabled: style.backgroundColor === null,
onChange: async (value) => {
this.refreshInput('backgroundRadius', value);
const v = parseFloat(value);
if (!isNaN(v)) {
await this.props.labelObject.setStyle({backgroundRadius: v});
}
}
});
}
this.setState({form});
}
/**
* Set an input value and make sure it's not disabled
*
* @param {string} name
* @param {*} value
*/
refreshInput(name, value) {
const form = [...this.state.form];
for (let i = 0; i < form.length; i++) {
if (form[i].name === name) {
form[i].value = value;
form[i].disabled = false;
}
}
this.setState({form});
}
/**
* Enable an input
*
* @param {string} name
*/
enableInput(name) {
const form = [...this.state.form];
for (let i = 0; i < form.length; i++) {
if (form[i].name === name) {
form[i].disabled = false;
}
}
this.setState({form});
}
/**
* Disable an input
*
* @param {string} name
*/
disableInput(name) {
const form = [...this.state.form];
for (let i = 0; i < form.length; i++) {
if (form[i].name === name) {
form[i].disabled = true;
}
}
this.setState({form});
}
/**
* Render the form
*
* @return {React.Component}
*/
renderForm() {
return (
<ScrollView>
{this.state.form.map(data => this.renderInput(data))}
</ScrollView>
);
}
/**
* Render the form input
*
* @param {object} data
* @return {void|React.Component}
*/
renderInput(data) {
if (data.disabled) {
return;
}
switch (data.type) {
case INPUT_TYPES.bool:
return this.renderBoolInput(data);
case INPUT_TYPES.text:
return this.renderTextInput(data);
case INPUT_TYPES.number:
return this.renderNumberInput(data);
case INPUT_TYPES.select:
return this.renderSelectInput(data);
case INPUT_TYPES.color:
return this.renderColorInput(data);
}
}
/**
* Render boolean input
*
* @param {object} data
* @return {React.Component}
*/
renderBoolInput({name, value, onChange}) {
return (
<View key={name} style={styles.inputRow}>
<Text style={styles.label}>{name}:</Text>
<Switch style={styles.input} onValueChange={onChange} value={value}/>
</View>
);
}
/**
* Render text input
*
* @param {object} data
* @return {React.Component}
*/
renderTextInput({name, onChange, value, multiline}) {
const numberOfLines = value.split('\n').length;
return (
<View key={name} style={styles.inputRow}>
<Text style={styles.label}>{name}:</Text>
<TextInput style={styles.input} value={String(value)} onChangeText={onChange} multiline={multiline} numberOfLines={numberOfLines}/>
</View>
);
}
/**
* Render number input
*
* @param {object} data
* @return {React.Component}
*/
renderNumberInput({name, onChange, value}) {
return (
<View key={name} style={styles.inputRow}>
<Text style={styles.label}>{name}:</Text>
<TextInput style={styles.input} keyboardType="numeric" value={String(value)} onChangeText={onChange}/>
</View>
);
}
/**
* Render select input
*
* @param {object} data
* @return {React.Component}
*/
renderSelectInput({name, value, onChange, choices}) {
return (
<View key={name} style={styles.inputRow}>
<Text style={styles.label}>{name}:</Text>
<Picker style={styles.picker} selectedValue={value} onValueChange={onChange}>
{choices.map((choice) => (<Picker.Item label={choice} key={choice} value={choice}/>))}
</Picker>
</View>
);
}
/**
* Render color input, when clicked it will change the state in order to display the color picker
*
* @param {object} data
* @return {React.Component}
*/
renderColorInput({value, name, onChange}) {
const overlayTextColor = tinycolor(value).isDark() ? '#FAFAFA' : '#222';
return (
<View key={name} style={styles.inputRow}>
<Text style={styles.label}>{name}:</Text>
<TouchableOpacity
onPress={() => this.setState({colorPicker: {onChange, value}})}
style={[styles.input, {backgroundColor: tinycolor(value).toHslString()}]}
>
<Text style={[styles.input, {color: overlayTextColor}]}>
{tinycolor(value).toHexString()}
</Text>
</TouchableOpacity>
</View>
);
}
/**
* Render the color picker if show be displayed
* @return {void|React.Component}
*/
renderColorPicker() {
if (this.state.colorPicker !== null) {
const {value, onChange} = this.state.colorPicker;
return (<SlidersColorPicker
visible={true}
color={value}
returnMode="hex"
onCancel={() => this.setState({colorPicker: null})}
onOk={(color) => {
this.setState({colorPicker: null});
onChange(color);
}}
swatches={['#247ba0', '#70c1b3', '#b2dbbf', '#f3ffbd', '#ff1654']}
swatchesLabel="PRESET"
okLabel="Done"
cancelLabel="Cancel"
/>);
}
}
render() {
return Platform.select({
ios:(
<KeyboardAvoidingView style={this.props.style} behavior={behavior} contentContainerStyle={{backgroundColor: "rgba(255, 255, 255, 0.3)"}} enabled>
{this.renderColorPicker()}
{this.renderForm()}
</KeyboardAvoidingView>
),
android: (
<View style={this.props.style} >
{this.renderColorPicker()}
{this.renderForm()}
</View>
)
});
}
}
Now we just need to include the EditComponent
when label is selected:
export default class EditComponent extends React.Component {
// ...
render() {
// Refactor the render method in order to include the toolbar
return (
<View style={styles.container}>
{this.renderToolbar()}
{this.renderWebView()}
{this.renderEditLabelView()}
</View>
);
}
renderEditLabelView() {
if (this.state.edit) {
return (<EditComponent style={styles.editComponent} labelObject={this.currentLabelEdit}/>);
}
}
// ...
}
Create a Label Text
We want to be able to create a label text on click, so let's modify the current code in order to add a mode switch and adapt the click callback.
const MODES = {
edit: 'edit',
createText: 'createText',
};
export default class App extends React.Component {
/**
* No Changes
*/
constructor() {
super();
this.state = {
ready: false,
mode: MODES.edit,
edit: false,
};
}
componentWillMount() {
// Create an entityManager using the API credentials (see AdsumClientAPI documentation for more details)
this.entityManager = new EntityManager({
"endpoint": "https://api.adsum.io",
"site": 322,
"username": "323-device",
"key": "343169bf805f8abd5fa71a4f529594a654de6afbac70a2d867a8b458c526fb7d"
});
// Create the Map instance
this.adsumRnMap = new AdsumNativeMap({});
this.start();
}
async start() {
// Init the Map
await this.adsumRnMap.init({
entityManager: this.entityManager,
deviceId: 323,
});
this.adsumRnMap.mouseManager.addEventListener(MOUSE_EVENTS.click, this.onMapClicked.bind(this));
// Start the rendering
await this.adsumRnMap.start();
this.setState({ready: true});
}
async onMapClicked(event) {
switch (this.state.mode) {
case MODES.edit:
return this.edit(await this.getClickedLabel(event.intersects));
case MODES.createText:
return this.createLabelText(event);
}
}
setMode(mode) {
if (this.state.mode === MODES.edit) {
// Cancel any ongoing edition
this.edit(null);
}
this.setState({ mode });
}
render() {
// Refactor the render method in order to include the toolbar
return (
<View style={styles.container}>
{this.renderToolbar()}
{this.renderWebView()}
{this.renderMenu()}
{this.renderEditLabelView()}
</View>
);
}
renderMenu() {
if (this.state.edit) {
return;
}
return (
<Picker selectedValue={this.state.mode} onValueChange={(mode) => this.setMode(mode)}>
<Picker.Item label="Edit Label" key={MODES.edit} value={MODES.edit}/>
<Picker.Item label="Create Label Text" key={MODES.createText} value={MODES.createText}/>
);
}
// ...
};
Now let's implements createLabelText
method:
async createLabelText(mouseEvent) {
if (mouseEvent.intersects.length === 0) {
return;
}
const { object, position } = mouseEvent.intersects[0];
if (object.isSite || object.isBuilding || object.isFloor || object.isSpace) {
const label = await this.adsumRnMap.objectManager.createLabelTextObject({
text: 'Hello world !\nI am using default properties.',
offset: position,
});
await this.adsumRnMap.objectManager.addLabel(label, object);
}
}
Note that we can endup with multiple labels on the same Space or Building which is not supported by the edition feature implemented earlier but that's totally possible.
Create Label Image
Let's add the new MODES switch
const MODES = {
edit: 'edit',
createText: 'createText',
createImage: 'createImage',
};
renderMenu() {
if (this.state.edit) {
return;
}
return (
<Picker selectedValue={this.state.mode} onValueChange={(mode) => this.setMode(mode)}>
<Picker.Item label="Edit Label" key={MODES.edit} value={MODES.edit}/>
<Picker.Item label="Create Label Text" key={MODES.createText} value={MODES.createText}/>
<Picker.Item label="Create Label Image" key={MODES.createImage} value={MODES.createImage}/>
</Picker>
);
}
async onMapClicked(event) {
switch (this.state.mode) {
case MODES.edit:
return this.edit(await this.getClickedLabel(event.intersects));
case MODES.createText:
return this.createLabelText(event);
case MODES.createImage:
return this.createLabelImage(event);
}
}
Now implements the createLabelImage
WARNING: Creating Label Image is a bit special. The image source can be a data image or any valid url. As the map is rendered in a Webview, a valid url is a URL accessible from the webview.
Here we are using data image in order to make it easier, but we recommend using react-native-static-server with react-native-fs in order to support offline label creation.
export default class App extends React.Component {
// ...
async createLabelImage(mouseEvent) {
if (mouseEvent.intersects.length === 0) {
return;
}
const { object, position } = mouseEvent.intersects[0];
if (object.isSite || object.isBuilding || object.isFloor || object.isSpace) {
const label = await this.adsumRnMap.objectManager.createLabelImageObject({
image: dataImage,
offset: position,
width: 60,
height: 60,
});
await this.adsumRnMap.objectManager.addLabel(label, object);
}
}
}
Note: It is highly adviced to provide image in power of 2, i.e. both width & height are power of two. In order to make one you can add transparent area to fill up to the next power of two.
Finally dataImage
is imported from another file:
import dataImage from "./assets/dataImage";
CAUTION: using too big image will affect strongly performances and might even crash on low devices.
Delete a Label
Let's add the new MODES switch
const MODES = {
edit: 'edit',
createText: 'createText',
createImage: 'createImage',
remove: 'remove',
};
renderMenu() {
if (this.state.edit) {
return;
}
return (
<Picker selectedValue={this.state.mode} onValueChange={(mode) => this.setMode(mode)}>
<Picker.Item label="Edit Label" key={MODES.edit} value={MODES.edit}/>
<Picker.Item label="Create Label Text" key={MODES.createText} value={MODES.createText}/>
<Picker.Item label="Create Label Image" key={MODES.createImage} value={MODES.createImage}/>
<Picker.Item label="Remove Label" key={MODES.remove} value={MODES.remove}/>
</Picker>
);
}
async onMapClicked(event) {
switch (this.state.mode) {
case MODES.edit:
return this.edit(await this.getClickedLabel(event.intersects));
case MODES.createText:
return this.createLabelText(event);
case MODES.createImage:
return this.createLabelImage(event);
case MODES.remove:
return this.remove(event);
}
}
And implements removeLabel
:
async removeLabel(mouseEvent) {
if (mouseEvent.intersects.length === 0) {
return;
}
const { object } = mouseEvent.intersects[0];
if (object.isLabel) {
await this.adsumRnMap.objectManager.removeLabel(object);
} else if (object.isBuilding || object.isSpace) {
const labels = await object.getLabels();
await Promise.all(labels.map(label => this.adsumRnMap.objectManager.removeLabel(label)));
}
}
Level of Details
Description
Level of details is the feature that change the visibility of the labels depending on the distance to the camera.
A LevelOfDetails is composed by several LevelStateInterface which are applied to the object at a certain distance.
For example, if we want our label to be visible at low distance (0 to 100 meters), then invisible at intermediate distance (100 to 200 meters) and visible at high distances (over 200 meters) we will do the following levels:
- 0: DisplayLevelState with DISPLAY_MODES.VISIBLE
- 100: DisplayLevelState with DISPLAY_MODES.NONE
- 200: DisplayLevelState with DISPLAY_MODES.VISIBLE
Note: if a label is selected, then the visibility will overwrite the one defined by the level of details.
See LabelVisibility for more details.
Let's improve our example by adding a way to edit LabelObject#levelOfDetails
Implementation
Let's create the LevelOfDetailsComponents
import React from "react";
import {Picker, Button, Text, View, ScrollView, StyleSheet, TextInput, Switch} from 'react-native';
import {Table, Row, TableWrapper, Cell} from 'react-native-table-component';
import {DISPLAY_MODES, DisplayLevelState} from '@adactive/adsum-react-native-map';
const styles = StyleSheet.create({
container: {
flex: 1, flexDirection: 'column', justifyContent: 'flex-end', backgroundColor: 'rgba(0,0,0,0.8)',
},
head: {height: 40, backgroundColor: '#f1f8ff'},
text: {margin: 6, backgroundColor: '#ffffff'},
row: {flexDirection: 'row', backgroundColor: '#FFF1C1', justifyContent: 'center'},
});
export default class LevelOfDetailsComponent extends React.Component {
state = {
/**
* Does the current LevelOfDetails is active ?
*
* @type {boolean}
*/
active: true,
/**
* The level states composing the level of details
*
* @type {LevelStateInterface[]}
*/
levelStates: [],
/**
* The display mode input for the adding row
*
* @type {DISPLAY_MODES.VISIBLE|DISPLAY_MODES.NONE}
*/
displayModeInput: DISPLAY_MODES.VISIBLE,
/**
* The start at input for the adding row
*
* @type {string}
*/
inputStartAt: '0',
};
componentDidMount() {
// Update levelOfDetails state
this.updateLevelOfDetails();
}
UNSAFE_componentWillReceiveProps() {
// Update levelOfDetails state
this.updateLevelOfDetails();
}
async updateLevelOfDetails() {
const levelStates = await this.props.labelObject.levelOfDetails.getLevelStates();
const active = await this.props.labelObject.levelOfDetails.isActive();
this.setState({levelStates, active});
}
async onRemove(startAt) {
await this.props.labelObject.levelOfDetails.removeLevelState(startAt);
this.updateLevelOfDetails();
}
async onAdd() {
const startAt = parseFloat(this.state.inputStartAt);
await this.props.labelObject.levelOfDetails.addLevelState(
startAt,
new DisplayLevelState(this.state.displayModeInput),
);
this.updateLevelOfDetails();
}
renderRemoveButton(startAt) {
return (<Button title="REMOVE" onPress={() => this.onRemove(startAt)}/>);
}
renderAddRow() {
const startAtInput = (
<TextInput
value={this.state.inputStartAt}
onChangeText={(text) => {
this.setState({inputStartAt: text});
}}
/>
);
const displayModeInput = (
<Picker
selectedValue={this.state.displayModeInput}
onValueChange={(value) => {
this.setState({displayModeInput: value});
}}
>
<Picker.Item
label={DISPLAY_MODES.VISIBLE}
key={DISPLAY_MODES.VISIBLE}
value={DISPLAY_MODES.VISIBLE}
/>
<Picker.Item
label={DISPLAY_MODES.NONE}
key={DISPLAY_MODES.NONE}
value={DISPLAY_MODES.NONE}
/>
</Picker>
);
const addBtn = (<Button title="ADD" onPress={() => this.onAdd()}/>);
return (
<TableWrapper key={-1} style={styles.row}>
<Cell key="startAt" data={startAtInput} textStyle={styles.text}/>
<Cell key="displayMode" data={displayModeInput} textStyle={styles.text}/>
<Cell key="add" data={addBtn}/>
</TableWrapper>
);
}
async onChangeActive(active) {
this.setState({active});
await this.props.labelObject.levelOfDetails.setActive(active);
}
renderLevelStateRow(startAt, levelState, key) {
const displayMode = levelState instanceof DisplayLevelState ? levelState.displayMode : 'Unknown Level State' ;
return (
<TableWrapper key={key} style={styles.row}>
<Cell key="startAt" data={startAt} textStyle={styles.text}/>
<Cell key="displayMode" data={displayMode} textStyle={styles.text}/>
<Cell key="remove" data={this.renderRemoveButton(startAt)}/>
</TableWrapper>
);
}
render() {
return (
<ScrollView>
<View style={styles.inputRow}>
<Text style={styles.label}>Level of Details is active:</Text>
<Switch style={styles.input} onValueChange={(value) => {
this.onChangeActive(value)
}} value={this.state.active}/>
</View>
<Text>Levels States:</Text>
<Table borderStyle={{borderWidth: 2, borderColor: '#c8e1ff'}}>
<Row data={['StartAt', 'DisplayMode', 'Action']} style={styles.head} textStyle={styles.text}/>
{
this.state.levelStates.map(({startAt, levelState}, index) => this.renderLevelStateRow(startAt, levelState, index))
}
{this.renderAddRow()}
</Table>
</ScrollView>
);
}
}
Note we have added react-native-table-component
As edit
select the label and considering Label Visibility we don't want
to select the label, so let's create a new MODE.
const MODES = {
edit: 'edit',
createText: 'createText',
createImage: 'createImage',
remove: 'remove',
levelOfDetails: 'levelOfDetails',
};
renderMenu() {
if (this.state.edit || this.state.editLevelOfDetails) {
return;
}
return (
<Picker selectedValue={this.state.mode} onValueChange={(mode) => this.setMode(mode)}>
<Picker.Item label="Edit Label" key={MODES.edit} value={MODES.edit}/>
<Picker.Item label="Create Label Text" key={MODES.createText} value={MODES.createText}/>
<Picker.Item label="Create Label Image" key={MODES.createImage} value={MODES.createImage}/>
<Picker.Item label="Remove Label" key={MODES.remove} value={MODES.remove}/>
<Picker.Item label="Level of Details" key={MODES.levelOfDetails} value={MODES.levelOfDetails}/>
</Picker>
);
}
async onMapClicked(event) {
switch (this.state.mode) {
case MODES.edit:
return this.edit(await this.getClickedLabel(event.intersects));
case MODES.createText:
return this.createLabelText(event);
case MODES.createImage:
return this.createLabelImage(event);
case MODES.remove:
return this.remove(event);
case MODES.levelOfDetails:
return this.editLevelOfDetails(await this.getClickedLabel(event.intersects));
}
}
And render the new component
render() {
// Refactor the render method in order to include the toolbar
return (
<View style={styles.container}>
{this.renderToolbar()}
{this.renderWebView()}
{this.renderMenu()}
{this.renderEditLabelView()}
{this.renderEditLevelOfDetails()}
</View>
);
}
renderEditLevelOfDetails() {
if (this.state.editLevelOfDetails) {
return (<LevelOfDetailsComponent style={styles.editComponent} labelObject={this.currentLabelEdit}/>);
}
}
You can find complete code here