(this.m_elemEditorTab = node)}
73 | />
74 | );
75 |
76 | let section_b =
;
77 |
78 | switch (editorTabSide) {
79 | case Side.Right:
80 | tmpComponent = section_a;
81 | section_a = section_b;
82 | section_b = tmpComponent;
83 | break;
84 | case Side.Top:
85 | layout = "vertical";
86 | break;
87 | case Side.Bottom:
88 | layout = "vertical";
89 | tmpComponent = section_a;
90 | section_a = section_b;
91 | section_b = tmpComponent;
92 | break;
93 | }
94 |
95 | content = (
96 |
mapHandler.resize()}
102 | />
103 | );
104 | }
105 |
106 | return {content}
;
107 | }
108 |
109 | private appendEditor() {
110 | if (this.m_elemEditorTab === null) {
111 | return;
112 | }
113 | this.m_elemEditorTab!.insertBefore(
114 | TextEditor.elemEditor,
115 | this.m_elemEditorTab!.children[0]
116 | );
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2017-2020 HERE Europe B.V.
3 | * Licensed under Apache 2.0, see full license in LICENSE
4 | * SPDX-License-Identifier: Apache-2.0
5 | */
6 | /**
7 | * Base command type for browser messaging
8 | */
9 | interface Command {
10 | command: string;
11 | }
12 |
13 | export interface Notification {
14 | message: string;
15 | severity: number;
16 | startColumn: number;
17 | startLineNumber: number;
18 | }
19 |
20 | export interface SetSourceValue extends Command {
21 | command: "SetSourceValue";
22 | value: string;
23 | }
24 |
25 | export interface GetSourceValue extends Command {
26 | command: "GetSourceValue";
27 | }
28 |
29 | export interface UpdateSourceValue extends Command {
30 | command: "UpdateSourceValue";
31 | line: number;
32 | column: number;
33 | value: string;
34 | }
35 |
36 | export interface Format extends Command {
37 | command: "Format";
38 | }
39 |
40 | export interface ShowCommands extends Command {
41 | command: "ShowCommands";
42 | }
43 |
44 | export interface UpdateCursorPosition extends Command {
45 | command: "UpdateCursorPosition";
46 | line: number;
47 | column: number;
48 | }
49 |
50 | export interface Init extends Command {
51 | command: "Init";
52 | }
53 |
54 | export interface InitData extends Command {
55 | command: "InitData";
56 | line: number;
57 | column: number;
58 | value: string;
59 | notificationsVisible: boolean;
60 | notificationsSize: number;
61 | }
62 |
63 | export interface SetCursor extends Command {
64 | command: "SetCursor";
65 | line: number;
66 | column: number;
67 | }
68 |
69 | export interface Undo extends Command {
70 | command: "undo";
71 | }
72 |
73 | export interface Redo extends Command {
74 | command: "redo";
75 | }
76 |
77 | export interface ToggleNotifications extends Command {
78 | command: "ToggleNotifications";
79 | notificationsVisible: boolean;
80 | notificationsSize: number;
81 | }
82 |
83 | export interface UpdateNotificationsCount extends Command {
84 | command: "UpdateNotificationsCount";
85 | count: number;
86 | severity: number;
87 | }
88 |
89 | export interface UpdateNotificationsSize extends Command {
90 | command: "UpdateNotificationsSize";
91 | UpdateNotificationsSize: number;
92 | }
93 |
94 | export interface HighlightFeature extends Command {
95 | command: "HighlightFeature";
96 | condition: string;
97 | }
98 |
99 | /**
100 | * Type that collect all available messages. This messages used for connect the text editor with the
101 | * map style editor
102 | */
103 | export type WindowCommands =
104 | | HighlightFeature
105 | | SetSourceValue
106 | | GetSourceValue
107 | | Format
108 | | Init
109 | | UpdateSourceValue
110 | | InitData
111 | | ShowCommands
112 | | Undo
113 | | Redo
114 | | UpdateCursorPosition
115 | | ToggleNotifications
116 | | UpdateNotificationsSize
117 | | UpdateNotificationsCount
118 | | SetCursor;
119 |
120 | /**
121 | * Contains all available positions for the text editor window
122 | */
123 | export enum Side {
124 | Left = "left",
125 | Right = "right",
126 | Top = "top",
127 | Bottom = "bottom",
128 | DeTouch = "float",
129 | }
130 |
131 | /**
132 | * Popup window interface
133 | */
134 | export interface Popup {
135 | component: JSX.Element;
136 | name: string;
137 | className?: string;
138 | id?: string;
139 | options?: {
140 | exitGuard?: "doNotExt" | "closeButton";
141 | };
142 | }
143 |
144 | export type Techniques =
145 | | "solid-line"
146 | | "dashed-line"
147 | | "line"
148 | | "fill"
149 | | "text"
150 | | "labeled-icon"
151 | | "none";
152 |
153 | export class TechniqueData {
154 | layer?: string;
155 | geometryType?: GeometryType;
156 | technique?: Techniques;
157 | description?: string;
158 | when?: string;
159 | }
160 |
161 | export type GeometryType = "line" | "polygon" | "point";
162 |
163 | export interface WhenPropsData {
164 | $geometryType: GeometryType;
165 | $layer: string;
166 | $id?: number;
167 | $level?: number;
168 | min_zoom?: number;
169 | kind?: string;
170 | kind_detail?: string;
171 |
172 | network?: string;
173 | }
174 |
--------------------------------------------------------------------------------
/src/map-editor/components-smart/PopupsContainer.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2017-2020 HERE Europe B.V.
3 | * Licensed under Apache 2.0, see full license in LICENSE
4 | * SPDX-License-Identifier: Apache-2.0
5 | */
6 | import * as React from "react";
7 | import { Popup } from "../../types";
8 | import Component, { SettingsState } from "../Component";
9 | import ButtonIcon, { ICONS } from "../components/ButtonIcon";
10 | import settings from "../Settings";
11 |
12 | /**
13 | * Responsible for showing popups on top of the other elements.
14 | */
15 | export default class PopupsContainer extends Component {
16 | static alertPopup(name: string, message: string) {
17 | const popups = settings.getStoreData("popups")!.slice();
18 | popups.push({
19 | name,
20 | component: {message},
21 | options: {},
22 | });
23 | settings.setStoreData("popups", popups);
24 | }
25 |
26 | static addPopup(popup: Popup) {
27 | const popups = settings.getStoreData("popups")!.slice();
28 | popups.push(popup);
29 | settings.setStoreData("popups", popups);
30 | }
31 |
32 | static removePopup(popup: Popup) {
33 | settings.setStoreData(
34 | "popups",
35 | settings.getStoreData("popups")!.filter((item: Popup) => item !== popup)
36 | );
37 | }
38 |
39 | constructor(props: {}) {
40 | super(props);
41 | this.state = {
42 | settings: {},
43 | store: {},
44 | };
45 |
46 | window.addEventListener("keyup", (event) => {
47 | const popups = this.state.store.popups as Popup[];
48 | if (event.key !== "Escape" || popups.length === 0) {
49 | return;
50 | }
51 |
52 | const popup = popups[popups.length - 1];
53 | const options = popup.options || {};
54 | if (options.exitGuard === undefined) {
55 | this.closePopup(popup);
56 | event.preventDefault();
57 | }
58 | });
59 | }
60 |
61 | componentDidMount() {
62 | this.connectStore(["popups"]);
63 | }
64 |
65 | render() {
66 | const popups = (this.state.store.popups as Popup[]) || [];
67 |
68 | return (
69 |
106 | );
107 | }
108 |
109 | private closePopup(popup: Popup) {
110 | if (popup.options && popup.options.exitGuard === "doNotExt") {
111 | return;
112 | }
113 | PopupsContainer.removePopup(popup);
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/src/map-editor/components-smart/MapElem.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2017-2020 HERE Europe B.V.
3 | * Licensed under Apache 2.0, see full license in LICENSE
4 | * SPDX-License-Identifier: Apache-2.0
5 | */
6 | import { MapControlsUI } from "@here/harp-map-controls";
7 | import * as React from "react";
8 | import Component, { SettingsState } from "../Component";
9 | import MapHandler from "../map-handler";
10 | import Info from "./Info";
11 | import Menu from "./Menu";
12 |
13 | interface Props {
14 | auto_resize: boolean;
15 | }
16 |
17 | /**
18 | * Responsible for managing DOM element of the map.
19 | */
20 | export default class extends Component {
21 | private m_elemCopyright: HTMLDivElement | null = null;
22 | private m_controlsContainer: HTMLDivElement | null = null;
23 | private m_mapControlsUI: MapControlsUI | null = null;
24 |
25 | private onMapRemoved: () => void;
26 | private onMapCreated: () => void;
27 | private onResize: () => void;
28 |
29 | constructor(props: Props) {
30 | super(props);
31 |
32 | this.state = {
33 | settings: {},
34 | store: {},
35 | };
36 |
37 | this.onResize = () => {
38 | if (this.props.auto_resize === true) {
39 | MapHandler.resize();
40 | }
41 | };
42 |
43 | this.onMapRemoved = () => {
44 | if (this.m_mapControlsUI !== null) {
45 | // TODO: dispose to avoid memory leak. Uncomment next line after next release.
46 | // this.m_mapControlsUI.dispose();
47 | this.m_mapControlsUI.domElement.remove();
48 | }
49 | };
50 |
51 | this.onMapCreated = () => {
52 | if (this.m_controlsContainer === null || this.m_elemCopyright === null) {
53 | throw new Error();
54 | }
55 |
56 | if (MapHandler.elem === null) {
57 | const elem = document.querySelector("#map-container .map") as HTMLCanvasElement;
58 | if (elem === null) {
59 | throw new Error();
60 | }
61 | MapHandler.init(elem, this.m_elemCopyright);
62 | } else {
63 | const elem = document.getElementById("map-container");
64 | if (elem === null) {
65 | throw new Error();
66 | }
67 | const canvas = elem.querySelector("canvas");
68 | if (canvas !== null) {
69 | elem.removeChild(canvas);
70 | }
71 | elem.appendChild(MapHandler.elem);
72 |
73 | const copyrightElem = elem.querySelector("#copyright") as HTMLDivElement;
74 | copyrightElem.remove();
75 | elem.appendChild(MapHandler.copyrightElem as HTMLElement);
76 | }
77 |
78 | if (MapHandler.controls === null || MapHandler.mapView === null) {
79 | throw new Error();
80 | }
81 |
82 | this.m_mapControlsUI = new MapControlsUI(MapHandler.controls, { zoomLevel: "input" });
83 | this.m_controlsContainer.appendChild(this.m_mapControlsUI.domElement);
84 | };
85 | }
86 |
87 | componentDidMount() {
88 | this.connectSettings(["editorTabVisible", "editorTabSize", "editorTabSide"]);
89 |
90 | MapHandler.on("mapCreated", this.onMapCreated);
91 | MapHandler.on("mapRemoved", this.onMapRemoved);
92 | window.addEventListener("resize", this.onResize);
93 |
94 | this.onMapCreated();
95 | }
96 |
97 | componentDidUpdate() {
98 | MapHandler.resize();
99 | }
100 |
101 | componentWillUnmount() {
102 | super.componentWillUnmount();
103 |
104 | MapHandler.removeListener("mapCreated", this.onMapCreated);
105 | MapHandler.removeListener("mapRemoved", this.onMapRemoved);
106 | window.removeEventListener("resize", this.onResize);
107 | }
108 |
109 | render() {
110 | return (
111 |
112 |
(this.m_controlsContainer = node)} />
113 |
114 |
(this.m_elemCopyright = node)} />
115 |
116 |
117 |
118 | );
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/src/map-editor/components-smart/PopupSelectTheme.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2017-2020 HERE Europe B.V.
3 | * Licensed under Apache 2.0, see full license in LICENSE
4 | * SPDX-License-Identifier: Apache-2.0
5 | */
6 | import * as React from "react";
7 | import TextButton from "../../components/TextButton";
8 | import Component, { SettingsState } from "../Component";
9 | import Tabs, { Tab } from "../components/Tabs";
10 | import settings from "../Settings";
11 | import TextEditor from "../TextEditor";
12 |
13 | import * as themeBase from "@here/harp-map-theme/resources/berlin_tilezen_base.json";
14 | import * as themeReduced from "@here/harp-map-theme/resources/berlin_tilezen_day_reduced.json";
15 | import * as themeNight from "@here/harp-map-theme/resources/berlin_tilezen_night_reduced.json";
16 |
17 | const DEFAULT_THEMES = [
18 | {
19 | name: "Day",
20 | theme: JSON.stringify(themeBase as any, undefined, 2),
21 | },
22 | {
23 | name: "Day - reduced",
24 | theme: JSON.stringify(themeReduced as any, undefined, 2),
25 | },
26 | {
27 | name: "Night - reduced",
28 | theme: JSON.stringify(themeNight as any, undefined, 2),
29 | },
30 | ];
31 |
32 | interface Stae extends SettingsState {
33 | activeTab: Tab;
34 | }
35 |
36 | interface Props {
37 | done: () => void;
38 | }
39 |
40 | /**
41 | * Responsible for ability to change the theme style, and ability to load default themes.
42 | */
43 | export default class extends Component
{
44 | private m_tabs: Tab[];
45 |
46 | constructor(props: Props) {
47 | super(props);
48 | this.m_tabs = [];
49 |
50 | const styles = settings.getStoreData("styles");
51 | if (styles === undefined) {
52 | throw new Error();
53 | }
54 |
55 | if (styles.length === 0) {
56 | this.m_tabs.push({
57 | name: "Switch style",
58 | component: null,
59 | disabled: true,
60 | });
61 | } else {
62 | this.m_tabs.push({
63 | name: "Switch style",
64 | component: (
65 |
66 |
Select style to apply from theme.
67 |
68 | {styles.map((style: string, i: number) => {
69 | return (
70 | -
71 | {
73 | settings.set("editorCurrentStyle", style);
74 | this.props.done();
75 | }}
76 | >
77 | {style}
78 |
79 |
80 | );
81 | })}
82 |
83 |
84 | ),
85 | });
86 | }
87 |
88 | this.m_tabs.push({
89 | name: "Load default theme",
90 | component: (
91 |
92 |
Load default theme template
93 |
94 | {DEFAULT_THEMES.map((item, i) => {
95 | return (
96 | -
97 | {
99 | TextEditor.setValue(item.theme);
100 | this.props.done();
101 | }}
102 | >
103 | {item.name}
104 |
105 |
106 | );
107 | })}
108 |
109 |
110 | ),
111 | });
112 |
113 | this.state = {
114 | activeTab: this.m_tabs.filter((tab) => !tab.disabled)[0],
115 | store: {},
116 | settings: {},
117 | };
118 | }
119 |
120 | componentWillMount() {
121 | this.connectStore(["styles"]);
122 | }
123 |
124 | render() {
125 | return (
126 | this.setState({ activeTab: tab })}
130 | id="switch-style"
131 | />
132 | );
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/src/components/SplitView.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2017-2020 HERE Europe B.V.
3 | * Licensed under Apache 2.0, see full license in LICENSE
4 | * SPDX-License-Identifier: Apache-2.0
5 | */
6 | import * as React from "react";
7 |
8 | type Mode = "horizontal" | "vertical";
9 |
10 | interface Props {
11 | section_a: JSX.Element | string;
12 | section_b: JSX.Element | string;
13 | mode: Mode;
14 | separatorPosition: number;
15 | onChange?: (size: number) => void;
16 | onResizing?: (size: number) => void;
17 | safeGap?: number;
18 | separatorSize?: number;
19 | }
20 |
21 | interface DragStartPosition {
22 | x: number;
23 | y: number;
24 | }
25 |
26 | interface State {
27 | dragStartPosition: DragStartPosition | null;
28 | }
29 |
30 | export default class extends React.Component {
31 | private onSeparatorDragStart: (event: React.MouseEvent) => void;
32 | private onSeparatorDragStop: (event: MouseEvent) => void;
33 | private onSeparatorDrag: (event: MouseEvent) => void;
34 | private onResize: () => void;
35 | private m_container: HTMLDivElement | null = null;
36 | private m_section_a: HTMLElement | null = null;
37 | private m_section_b: HTMLElement | null = null;
38 | private m_separator: HTMLDivElement | null = null;
39 | private m_safeGap: number;
40 | private m_separatorSize: number;
41 | private m_separatorPosition: number;
42 | private m_separatorPositionStart = 0;
43 |
44 | constructor(props: Props) {
45 | super(props);
46 |
47 | this.m_safeGap = props.safeGap || 40;
48 | this.m_separatorSize = props.separatorSize || 4;
49 | this.m_separatorPosition = props.separatorPosition || 4;
50 |
51 | this.state = {
52 | dragStartPosition: null,
53 | };
54 |
55 | this.onSeparatorDragStart = (event: React.MouseEvent) => {
56 | this.setState({
57 | dragStartPosition: {
58 | x: event.clientX,
59 | y: event.clientY,
60 | },
61 | });
62 |
63 | this.m_separatorPositionStart = this.m_separatorPosition;
64 |
65 | window.addEventListener("mousemove", this.onSeparatorDrag);
66 |
67 | event.preventDefault();
68 | event.stopPropagation();
69 | };
70 |
71 | this.onSeparatorDragStop = (event: MouseEvent) => {
72 | if (!this.state.dragStartPosition === null) {
73 | return;
74 | }
75 |
76 | window.removeEventListener("mousemove", this.onSeparatorDrag);
77 |
78 | this.onSeparatorDrag(event);
79 | this.setState({ dragStartPosition: null });
80 |
81 | if (this.props.onChange !== undefined) {
82 | this.props.onChange(this.m_separatorPosition);
83 | }
84 | };
85 |
86 | this.onSeparatorDrag = (event: MouseEvent) => {
87 | if (!this.state.dragStartPosition) {
88 | return;
89 | }
90 |
91 | const pos = { x: event.clientX, y: event.clientY };
92 | const startPos = this.state.dragStartPosition;
93 | let separatorPosition = this.m_separatorPositionStart;
94 |
95 | switch (this.props.mode) {
96 | case "horizontal":
97 | separatorPosition += pos.x - startPos.x;
98 | break;
99 | break;
100 | case "vertical":
101 | separatorPosition += pos.y - startPos.y;
102 | break;
103 | }
104 |
105 | this.setSizes(separatorPosition);
106 | event.preventDefault();
107 | event.stopPropagation();
108 | };
109 |
110 | this.onResize = () => {
111 | this.setSizes(this.m_separatorPosition);
112 | };
113 | }
114 |
115 | componentDidMount() {
116 | window.addEventListener("mouseup", this.onSeparatorDragStop);
117 | window.addEventListener("mouseleave", this.onSeparatorDragStop);
118 | window.addEventListener("resize", this.onResize);
119 | this.setSizes(this.m_separatorPosition);
120 | }
121 |
122 | componentWillUnmount() {
123 | window.removeEventListener("mouseup", this.onSeparatorDragStop);
124 | window.removeEventListener("mouseleave", this.onSeparatorDragStop);
125 | window.removeEventListener("resize", this.onResize);
126 | }
127 |
128 | render() {
129 | this.setSizes(this.m_separatorPosition);
130 |
131 | const mouseCatcher =
132 | this.state.dragStartPosition === null ? null : ;
133 |
134 | return (
135 | (this.m_container = node)}
138 | >
139 |
(this.m_section_a = node)}>{this.props.section_a}
140 |
(this.m_section_b = node)}>{this.props.section_b}
141 |
(this.m_separator = node)}
145 | />
146 | {mouseCatcher}
147 |
148 | );
149 | }
150 |
151 | private setSizes(size: number) {
152 | const { m_container, m_section_a, m_section_b, m_separator } = this;
153 |
154 | if (
155 | m_container === null ||
156 | m_section_a === null ||
157 | m_section_b === null ||
158 | m_separator === null
159 | ) {
160 | return;
161 | }
162 |
163 | const rect = m_container.getBoundingClientRect();
164 | size = Math.max(size, this.m_safeGap);
165 | let maxSize = 0;
166 | switch (this.props.mode) {
167 | case "horizontal":
168 | size = Math.min(size, rect.width - this.m_safeGap - this.m_separatorSize);
169 | maxSize = rect.width;
170 | break;
171 | case "vertical":
172 | size = Math.min(size, rect.height - this.m_safeGap - this.m_separatorSize);
173 | maxSize = rect.height;
174 | break;
175 | }
176 |
177 | const propFirst = this.props.mode === "horizontal" ? "left" : "top";
178 | const propSecond = this.props.mode === "horizontal" ? "right" : "bottom";
179 |
180 | for (const element of [m_section_a, m_section_b, m_separator]) {
181 | element.style.left = "0";
182 | element.style.right = "0";
183 | element.style.top = "0";
184 | element.style.bottom = "0";
185 | }
186 |
187 | m_section_a.style[propSecond] = `${maxSize - size}px`;
188 |
189 | m_separator.style[propFirst] = `${size}px`;
190 | m_separator.style[propSecond] = `${maxSize - size - this.m_separatorSize}px`;
191 |
192 | m_section_b.style[propFirst] = `${size + this.m_separatorSize}px`;
193 |
194 | this.m_separatorPosition = size;
195 |
196 | if (this.props.onResizing) {
197 | this.props.onResizing(this.m_separatorPosition);
198 | }
199 | }
200 | }
201 |
--------------------------------------------------------------------------------
/src/map-editor/components-smart/PopupCreateTechnique.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2017-2020 HERE Europe B.V.
3 | * Licensed under Apache 2.0, see full license in LICENSE
4 | * SPDX-License-Identifier: Apache-2.0
5 | */
6 | import { Style } from "@here/harp-datasource-protocol";
7 | import { Expr } from "@here/harp-datasource-protocol/lib/Expr";
8 | import * as React from "react";
9 | import { GeometryType, TechniqueData, Techniques } from "../../types";
10 | import Component, { SettingsState } from "../Component";
11 | import ButtonIcon, { ICONS } from "../components/ButtonIcon";
12 | import SelectString from "../components/SelectString";
13 | import * as DATASOURCE_SCHEMA from "../datasourceSchemaModified.json";
14 | import MapHandler from "../map-handler";
15 | import PopupsContainer from "./PopupsContainer";
16 |
17 | interface State extends SettingsState {
18 | techniqueData: TechniqueData;
19 | }
20 |
21 | interface LayerData {
22 | geometry_types: GeometryType[];
23 | properties: {
24 | [key: string]: {
25 | [key: string]: string;
26 | };
27 | };
28 | }
29 |
30 | const GEOMETRY_TECHNiQUES: { [key in GeometryType]: Techniques[] } = {
31 | line: ["none", "solid-line", "dashed-line", "line"],
32 | polygon: ["none", "fill", "solid-line", "dashed-line", "line"],
33 | point: ["none", "text", "labeled-icon"],
34 | };
35 |
36 | const LAYERS = Object.keys(DATASOURCE_SCHEMA);
37 |
38 | interface Props {
39 | done: () => void;
40 | techniqueData?: TechniqueData;
41 | }
42 |
43 | /**
44 | * Responsible for ability to change the theme style, and ability to load default themes.
45 | */
46 | export default class extends Component
{
47 | private m_input: HTMLInputElement | null = null;
48 |
49 | constructor(props: Props) {
50 | super(props);
51 | this.state = {
52 | settings: {},
53 | store: {},
54 | techniqueData: props.techniqueData || new TechniqueData(),
55 | };
56 | }
57 |
58 | componentWillMount() {
59 | this.connectStore(["styles"]);
60 | }
61 |
62 | addStyle() {
63 | const { techniqueData } = this.state;
64 | if (techniqueData.technique === undefined) {
65 | throw new Error();
66 | }
67 |
68 | if (techniqueData.when === undefined) {
69 | PopupsContainer.alertPopup("Error", "Style is missing mandatory when condition.");
70 | return;
71 | }
72 |
73 | const style = {
74 | technique: techniqueData.technique,
75 | when:
76 | typeof techniqueData.when === "string"
77 | ? Expr.parse(techniqueData.when).toJSON()
78 | : techniqueData.when,
79 | description: techniqueData.description,
80 | attr: {},
81 | };
82 |
83 | switch (MapHandler.addStyleTechnique(style as Style)) {
84 | case "err":
85 | PopupsContainer.alertPopup("Error", "Can't create style.");
86 | break;
87 | case "exists":
88 | PopupsContainer.alertPopup("Error", "Description is already exists.");
89 | break;
90 | case "ok":
91 | this.props.done();
92 | break;
93 | }
94 | }
95 |
96 | render() {
97 | let currentPage = null;
98 | const { techniqueData } = this.state;
99 |
100 | if (techniqueData.layer === undefined) {
101 | currentPage = (
102 |
103 |
Select Layer
104 | {
108 | techniqueData.layer = val;
109 | this.setState({ techniqueData });
110 | }}
111 | />
112 |
113 | );
114 | } else if (techniqueData.geometryType === undefined) {
115 | // @ts-ignore: Element implicitly has an 'any' type
116 | const layerData = DATASOURCE_SCHEMA[techniqueData.layer as string] as LayerData;
117 |
118 | currentPage = (
119 |
120 |
Select geometry type
121 | {
125 | techniqueData.geometryType = val;
126 | this.setState({ techniqueData });
127 | }}
128 | />
129 |
130 | );
131 | } else if (techniqueData.technique === undefined) {
132 | if (techniqueData.geometryType === undefined) {
133 | throw new Error();
134 | }
135 | const techniques = GEOMETRY_TECHNiQUES[techniqueData.geometryType];
136 | currentPage = (
137 |
138 |
Select technique
139 | {
143 | techniqueData.technique = val;
144 | this.setState({ techniqueData });
145 | }}
146 | />
147 |
148 | );
149 | } else if (techniqueData.when === undefined) {
150 | if (techniqueData.geometryType === undefined) {
151 | throw new Error();
152 | }
153 | // @ts-ignore: Element implicitly has an 'any' type
154 | const currentLayerData = DATASOURCE_SCHEMA[techniqueData.layer as string] as LayerData;
155 | // tslint:disable-next-line: max-line-length
156 | const defaultValue = `$layer == '${techniqueData.layer}' && $geometryType == '${techniqueData.geometryType}'`;
157 |
158 | currentPage = (
159 |
160 |
Set "when" selector field
161 |
162 | {Object.entries(currentLayerData.properties).map(([section, props], i) => {
163 | return (
164 |
165 | {section}
166 |
167 | {Object.entries(props).map(([key, val], j) => {
168 | return (
169 | -
170 | {key}:
171 | {(typeof val as string | object) === "string"
172 | ? val
173 | : JSON.stringify(val, undefined, 4)}
174 |
175 | );
176 | })}
177 |
178 |
179 | );
180 | })}
181 |
182 |
(this.m_input = elem)}
186 | defaultValue={defaultValue}
187 | />
188 |
{
192 | if (this.m_input === null || this.m_input.value.trim().length === 0) {
193 | PopupsContainer.alertPopup(
194 | "Warning!",
195 | `Selector field "when" should not be empty.`
196 | );
197 | return;
198 | }
199 | techniqueData.when = this.m_input.value.trim();
200 | this.m_input.value = "";
201 | this.setState({ techniqueData });
202 | }}
203 | />
204 |
205 | );
206 | } else if (techniqueData.description === undefined) {
207 | currentPage = (
208 |
209 |
Set description
210 | (this.m_input = elem)}
214 | defaultValue={techniqueData.description}
215 | />
216 | {
220 | if (this.m_input === null || this.m_input.value.trim().length === 0) {
221 | PopupsContainer.alertPopup(
222 | "Warning!",
223 | "Please add some description."
224 | );
225 | return;
226 | }
227 | techniqueData.description = this.m_input.value.trim();
228 | this.m_input.value = "";
229 | this.addStyle();
230 | }}
231 | />
232 |
233 | );
234 | }
235 |
236 | return {currentPage}
;
237 | }
238 | }
239 |
--------------------------------------------------------------------------------
/src/map-editor/components-smart/Menu.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2017-2020 HERE Europe B.V.
3 | * Licensed under Apache 2.0, see full license in LICENSE
4 | * SPDX-License-Identifier: Apache-2.0
5 | */
6 | import * as React from "react";
7 | import { Popup, Side, TechniqueData } from "../../types";
8 | import Component, { SettingsState } from "../Component";
9 | import ButtonIcon, { ButtonIconProps, ICONS } from "../components/ButtonIcon";
10 | import settings from "../Settings";
11 | import TextEditor from "../TextEditor";
12 | import PopupCreateTechnique from "./PopupCreateTechnique";
13 | import PopupGeometriesList from "./PopupGeometriesList";
14 | import PopupsContainer from "./PopupsContainer";
15 | import PopupSelectLink from "./PopupSelectLink";
16 | import PopupSelectTheme from "./PopupSelectTheme";
17 |
18 | enum MenuState {
19 | Idle,
20 | SelectSide,
21 | Hidden,
22 | }
23 |
24 | interface Props extends SettingsState {
25 | menuState: MenuState;
26 | }
27 |
28 | export type NotificationType = "secondary" | "warn" | "error";
29 |
30 | /**
31 | * Shows currently available actions for the user.
32 | */
33 | export default class Menu extends Component<{}, Props> {
34 | static openNewTechniquePopup(techniqueData?: TechniqueData) {
35 | const popup: Popup = {
36 | name: "Create technique",
37 | options: { exitGuard: "closeButton" },
38 | component: (
39 | PopupsContainer.removePopup(popup)}
41 | techniqueData={techniqueData}
42 | />
43 | ),
44 | };
45 | PopupsContainer.addPopup(popup);
46 | }
47 |
48 | constructor(props: {}) {
49 | super(props);
50 |
51 | this.state = {
52 | menuState: MenuState.Idle,
53 | settings: {},
54 | store: {},
55 | };
56 | }
57 |
58 | componentWillMount() {
59 | this.connectSettings([
60 | "editorTabSize",
61 | "editorTabVisible",
62 | "editorTabSide",
63 | "editorInfoPick",
64 | "notificationsVisible",
65 | ]);
66 | this.connectStore(["styles", "parsedTheme", "notificationsState"]);
67 | }
68 |
69 | render() {
70 | const editorTabSide = this.state.settings.editorTabSide as Side;
71 | const editorTabVisible = this.state.settings.editorTabVisible as boolean;
72 | let menuState = this.state.menuState;
73 |
74 | const themeIsValid = this.state.store.parsedTheme !== null;
75 |
76 | let buttons: ButtonIconProps[] = [
77 | {
78 | icon: ICONS.eye,
79 | active: editorTabVisible,
80 | title: "Show / Hide",
81 | onClick: () => {
82 | settings.emit("editor:toggle");
83 | },
84 | },
85 | ];
86 |
87 | if (!editorTabVisible) {
88 | menuState = MenuState.Hidden;
89 | }
90 |
91 | switch (menuState) {
92 | case MenuState.Idle:
93 | buttons.unshift(
94 | this.createGeometriesPopupButton(!themeIsValid),
95 | this.createThemePopupButton(),
96 | {
97 | icon: ICONS.download,
98 | title: "Download file",
99 | disabled: !themeIsValid,
100 | onClick: () => {
101 | TextEditor.download();
102 | },
103 | },
104 | {
105 | icon: ICONS.open,
106 | title: "Open file",
107 | onClick: () => {
108 | TextEditor.openFile();
109 | },
110 | },
111 | {
112 | icon: ICONS.format,
113 | title: "Format file",
114 | disabled: !themeIsValid,
115 | onClick: () => {
116 | TextEditor.formatFile();
117 | },
118 | },
119 | {
120 | icon: ICONS[editorTabSide],
121 | title: "Change text editor position",
122 | className: editorTabSide,
123 | onClick: () => {
124 | this.setState({ menuState: MenuState.SelectSide });
125 | },
126 | },
127 | {
128 | icon: ICONS.commands,
129 | title: "Show quick command palette",
130 | onClick: () => {
131 | TextEditor.showCommands();
132 | },
133 | },
134 | {
135 | icon: ICONS.undo,
136 | title: "Undo",
137 | onClick: () => {
138 | TextEditor.undo();
139 | },
140 | },
141 | {
142 | icon: ICONS.redo,
143 | title: "Redo",
144 | onClick: () => {
145 | TextEditor.redo();
146 | },
147 | },
148 | {
149 | icon: ICONS.link,
150 | title: "Get link",
151 | onClick: () => {
152 | settings.getSettingsURL().then((link) => {
153 | PopupsContainer.addPopup({
154 | id: "share-link-popup",
155 | name: "Link",
156 | component: ,
157 | });
158 | });
159 | },
160 | },
161 | {
162 | icon: ICONS.magicStick,
163 | title: "Construct new style technique",
164 | disabled: !themeIsValid,
165 | onClick: () => Menu.openNewTechniquePopup(),
166 | },
167 | {
168 | icon: ICONS.picker,
169 | title: "Toggle info pick",
170 | active: settings.get("editorInfoPick"),
171 | onClick: () => {
172 | settings.set("editorInfoPick", !settings.get("editorInfoPick"));
173 | },
174 | },
175 | this.createNotificationsButton()
176 | );
177 | break;
178 |
179 | case MenuState.SelectSide:
180 | buttons = [Side.Top, Side.Right, Side.Bottom, Side.Left, Side.DeTouch].map(
181 | (side, i) => {
182 | return {
183 | key: i,
184 | icon: ICONS[side],
185 | active: side === editorTabSide,
186 | title: side[0].toUpperCase() + side.slice(1),
187 | className: side,
188 | onClick: () => {
189 | settings.emit("editor:setSide", side);
190 | this.setState({ menuState: MenuState.Idle });
191 | },
192 | };
193 | }
194 | );
195 | break;
196 | }
197 |
198 | return (
199 |
215 | );
216 | }
217 |
218 | private createThemePopupButton(): ButtonIconProps {
219 | return {
220 | icon: ICONS.colorPalette,
221 | title: "Switch styles / Load default theme",
222 | onClick: () => {
223 | const popup = {
224 | name: "Switch styles",
225 | options: {},
226 | component: PopupsContainer.removePopup(popup)} />,
227 | };
228 | PopupsContainer.addPopup(popup);
229 | },
230 | };
231 | }
232 |
233 | private createGeometriesPopupButton(disabled: boolean): ButtonIconProps {
234 | return {
235 | icon: ICONS.geometries,
236 | title: "Geometries list",
237 | disabled,
238 | onClick: () => {
239 | const popup = {
240 | name: "Geometries list",
241 | options: {},
242 | component: (
243 | PopupsContainer.removePopup(popup)} />
244 | ),
245 | };
246 | PopupsContainer.addPopup(popup);
247 | },
248 | };
249 | }
250 |
251 | private createNotificationsButton(): ButtonIconProps {
252 | const notificationsState = settings.getStoreData("notificationsState");
253 | const notificationsVisible = settings.get("notificationsVisible");
254 |
255 | if (notificationsState === undefined) {
256 | throw new Error();
257 | }
258 |
259 | let state: NotificationType = "secondary";
260 |
261 | if (notificationsState.severity > 6) {
262 | state = "error";
263 | } else if (notificationsState.count > 0) {
264 | state = "warn";
265 | }
266 |
267 | return {
268 | icon: ICONS.alert,
269 | title: "Notifications",
270 | className: state,
271 | label: notificationsState.count + "",
272 | active: notificationsVisible,
273 | onClick: () => {
274 | settings.set("notificationsVisible", !notificationsVisible);
275 | },
276 | };
277 | }
278 | }
279 |
--------------------------------------------------------------------------------
/src/map-editor/Settings.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2017-2020 HERE Europe B.V.
3 | * Licensed under Apache 2.0, see full license in LICENSE
4 | * SPDX-License-Identifier: Apache-2.0
5 | */
6 | import { Theme } from "@here/harp-datasource-protocol";
7 | import * as theme from "@here/harp-map-theme/resources/berlin_tilezen_base.json";
8 | import { EventEmitter } from "events";
9 | import * as jszip from "jszip";
10 | import { throttle } from "throttle-debounce";
11 | import { Popup, Side } from "../types";
12 | import MapViewState from "./map-handler/MapViewState";
13 |
14 | /**
15 | * Describe settings interface and settings types.
16 | */
17 | export interface AvailableSetting {
18 | /**
19 | * The side in the browser where to be to the text editor.
20 | */
21 | editorTabSide: Side;
22 | /**
23 | * The size of the text editor. With or height depending on [[AvailableSetting.editorTabSide]].
24 | */
25 | editorTabSize: number;
26 | /**
27 | * Hidden or not the text editor.
28 | */
29 | editorTabVisible: boolean;
30 | /**
31 | * Toggle ability to pik info about elements on map.
32 | */
33 | editorInfoPick: boolean;
34 | /**
35 | * Current style from theme that currently uses the data source.
36 | */
37 | editorCurrentStyle: string | null;
38 | /**
39 | * Current column of the text editor cursor.
40 | */
41 | "textEditor:column": number;
42 | /**
43 | * Current line of the text editor cursor.
44 | */
45 | "textEditor:line": number;
46 | /**
47 | * Source code of the JSON Theme.
48 | */
49 | "textEditor:sourceCode": string;
50 | /**
51 | * Saves the position what we currently observing.
52 | */
53 | editorMapViewState: string;
54 | /**
55 | * Access Key for HERE data sources.
56 | */
57 | accessKeyId: string;
58 | /**
59 | * Secret access Key for HERE data sources.
60 | */
61 | accessKeySecret: string;
62 | /**
63 | * Toggle notifications visibility.
64 | */
65 | notificationsVisible: boolean;
66 | /**
67 | * Toggle notifications visibility.
68 | */
69 | notificationsSize: number;
70 | }
71 |
72 | /**
73 | * Describe store interface and store types.
74 | */
75 | export interface AvailableData {
76 | /**
77 | * Access Key for HERE data sources.
78 | */
79 | accessKeyId: string;
80 | /**
81 | * Secret access Key for HERE data sources.
82 | */
83 | accessKeySecret: string;
84 | /**
85 | * True if the current session is authorized and we get the bearer token.
86 | */
87 | authorized: boolean;
88 | /**
89 | * Contains current visible popups.
90 | */
91 | popups: Popup[];
92 | /**
93 | * Contains current available styles from the map theme.
94 | */
95 | styles: string[];
96 | /**
97 | * Last parsed source code of the theme. If equals [[null]] then the source code probably
98 | * invalid [[JSON]].
99 | */
100 | parsedTheme: Theme | null;
101 | /**
102 | * Contains current set notifications for user.
103 | */
104 | notificationsState: {
105 | count: number;
106 | severity: number;
107 | };
108 | }
109 |
110 | type Setting = string | number | boolean | Side | null;
111 |
112 | /**
113 | * Manages settings and store data, allow to observe changes thru events.
114 | */
115 | class Settings extends EventEmitter {
116 | //Key where to save user settings in localStore.
117 | readonly m_settingsName = "editorSettings";
118 | readonly m_restoreUrlParamName = "settings";
119 |
120 | /**
121 | * Save user settings to localStore immediately.
122 | */
123 | saveForce: () => void;
124 |
125 | /**
126 | * User settings stores here.
127 | */
128 | private m_settings: SType;
129 | /**
130 | * The data store.
131 | */
132 | private m_store: { [Key in keyof StType]?: StType[Key] };
133 |
134 | /**
135 | * Saves the settings data to localStore asynchronously
136 | */
137 | private save: () => void;
138 |
139 | constructor(
140 | settingsDefaults: SType,
141 | initialStoreData: { [Key in keyof StType]?: StType[Key] }
142 | ) {
143 | super();
144 |
145 | this.m_settings = settingsDefaults;
146 | this.m_store = initialStoreData;
147 |
148 | this.saveForce = () => {
149 | if (!localStorage) {
150 | return;
151 | }
152 |
153 | localStorage.setItem(this.m_settingsName, JSON.stringify(this.m_settings));
154 | };
155 |
156 | this.save = throttle(500, this.saveForce);
157 | }
158 |
159 | async init() {
160 | if (window.location.search !== "") {
161 | await this.loadFromSettingsURL();
162 | } else {
163 | this.load();
164 | }
165 |
166 | Object.entries(this.m_settings).forEach(([key, val]) => this.emit(key, val));
167 |
168 | window.addEventListener("beforeunload", () => {
169 | window.onbeforeunload = () => {
170 | this.saveForce();
171 | };
172 | });
173 | }
174 |
175 | /**
176 | * Sets specified setting ans saves it to localStore asynchronously.
177 | */
178 | set(key: A, val: B) {
179 | if (this.m_settings[key] === val) {
180 | return val;
181 | }
182 |
183 | this.m_settings[key] = val;
184 | this.save();
185 | this.emit(`setting:${key}`, val);
186 | return val;
187 | }
188 |
189 | /**
190 | * Returns value of specified setting.
191 | */
192 | get(key: A): B {
193 | if (this.m_settings.hasOwnProperty(key)) {
194 | return this.m_settings[key] as B;
195 | }
196 | throw new Error(`Setting "${key}" don't exist`);
197 | }
198 |
199 | /**
200 | * Sets specified data to the store by specified key.
201 | */
202 | setStoreData(key: A, val: B) {
203 | if (this.m_store[key] === val) {
204 | return val;
205 | }
206 |
207 | this.m_store[key] = val;
208 | this.emit(`store:${key}`, val);
209 | return val;
210 | }
211 |
212 | /**
213 | * Returns store data of specified key.
214 | */
215 | getStoreData(key: A): B | undefined {
216 | if (this.m_store.hasOwnProperty(key)) {
217 | return this.m_store[key] as B;
218 | }
219 | return undefined;
220 | }
221 |
222 | /**
223 | * Get multiple settings at once.
224 | */
225 | read(list: A[]): { [key in A]?: B } {
226 | const res: { [key in A]?: B } = {};
227 | for (const key of list) {
228 | if (this.m_settings.hasOwnProperty(key)) {
229 | res[key] = this.m_settings[key] as B;
230 | }
231 | }
232 | return res;
233 | }
234 |
235 | /**
236 | * Get multiple entries from store at once.
237 | */
238 | readStore(
239 | list: A[]
240 | ): { [key in keyof StType]?: StType[key] } {
241 | const res: { [key in keyof StType]?: StType[key] } = {};
242 | for (const key of list) {
243 | if (this.m_store.hasOwnProperty(key)) {
244 | res[key] = this.m_store[key] as B;
245 | }
246 | }
247 | return res;
248 | }
249 |
250 | /**
251 | * Generate URL from current settings state.
252 | */
253 | getSettingsURL() {
254 | const settingsCopy = JSON.stringify(this.m_settings);
255 |
256 | const zip = new jszip();
257 |
258 | zip.file("settings.json", settingsCopy);
259 |
260 | return zip
261 | .generateAsync({
262 | type: "base64",
263 | compression: "DEFLATE",
264 | compressionOptions: { level: 9 },
265 | })
266 | .then((content) => {
267 | // tslint:disable-next-line: max-line-length
268 | return `${window.location.origin}${window.location.pathname}?${this.m_restoreUrlParamName}=${content}`;
269 | });
270 | }
271 |
272 | /**
273 | * Load current settings state from [[window.location]].
274 | */
275 | async loadFromSettingsURL() {
276 | const query: { [key: string]: string | undefined } = {};
277 | window.location.search
278 | .slice(1)
279 | .split("&")
280 | .reduce((result, item) => {
281 | const index = item.indexOf("=");
282 | result[item.slice(0, index)] = item.slice(index + 1);
283 | return result;
284 | }, query);
285 |
286 | if (query[this.m_restoreUrlParamName] === undefined) {
287 | return;
288 | }
289 |
290 | window.history.pushState({}, "", window.location.origin + window.location.pathname);
291 | const zip = await jszip.loadAsync(query[this.m_restoreUrlParamName] as string, {
292 | base64: true,
293 | });
294 |
295 | if (!zip.files["settings.json"]) {
296 | return;
297 | }
298 |
299 | const jsonData = await zip.files["settings.json"].async("text");
300 |
301 | this.load(jsonData);
302 | }
303 |
304 | /**
305 | * Load and parse data from localStore
306 | */
307 | private load(strData: string | null = null) {
308 | if (!localStorage) {
309 | return;
310 | }
311 |
312 | const data = strData || localStorage.getItem(this.m_settingsName);
313 | if (data === null) {
314 | return;
315 | }
316 |
317 | const userSettings = JSON.parse(data);
318 | const keys = Object.keys(this.m_settings) as (keyof SType)[];
319 |
320 | keys.forEach((key) => {
321 | if (userSettings.hasOwnProperty(key)) {
322 | this.m_settings[key] = userSettings[key];
323 | }
324 | });
325 | }
326 | }
327 |
328 | // Create settings manager with defaults
329 | const settings = new Settings(
330 | {
331 | editorTabSide: Side.Left,
332 | editorTabSize: 600,
333 | editorTabVisible: true,
334 | editorInfoPick: false,
335 | editorCurrentStyle: null,
336 | editorMapViewState: new MapViewState().toString(),
337 | accessKeyId: "",
338 | accessKeySecret: "",
339 | notificationsVisible: false,
340 | notificationsSize: 800,
341 | "textEditor:column": 1,
342 | "textEditor:line": 1,
343 | "textEditor:sourceCode": JSON.stringify(theme as any, undefined, 4),
344 | },
345 | { popups: [], styles: [], notificationsState: { count: 0, severity: 0 } }
346 | );
347 |
348 | // Singleton settings manager
349 | export default settings;
350 |
--------------------------------------------------------------------------------
/src/text-editor-frame/TextEditor.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2017-2020 HERE Europe B.V.
3 | * Licensed under Apache 2.0, see full license in LICENSE
4 | * SPDX-License-Identifier: Apache-2.0
5 | */
6 | import { Style } from "@here/harp-datasource-protocol";
7 | import { Expr } from "@here/harp-datasource-protocol/lib/Expr";
8 | import { EventEmitter } from "events";
9 | import * as monaco from "monaco-editor";
10 | import { throttle } from "throttle-debounce";
11 | import { Notification, WindowCommands } from "../types";
12 | import * as schema from "./harp-theme.vscode.schema.json";
13 |
14 | type Commands = WindowCommands["command"];
15 |
16 | /**
17 | * This class controls the monaco editor and communicate thru messages with the Theme editor.
18 | */
19 | export class TextEditor extends EventEmitter {
20 | /**
21 | * The macro editor instance
22 | */
23 | private m_decorations: string[] = [];
24 | private m_editor: monaco.editor.IStandaloneCodeEditor | null = null;
25 | private m_monacoNotifications: Notification[] = [];
26 | private m_editorElem: HTMLElement | null = null;
27 |
28 | private m_modelUri = monaco.Uri.parse("a:/harp.gl/default.theme.json");
29 | private m_model = monaco.editor.createModel("", "json", this.m_modelUri);
30 |
31 | init() {
32 | this.m_editorElem = document.createElement("div");
33 |
34 | monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
35 | validate: true,
36 | schemas: [
37 | {
38 | uri: "https://github.com/heremaps/harp.gl/theme.scheme.json",
39 | fileMatch: [".theme.json"],
40 | schema,
41 | },
42 | ],
43 | });
44 |
45 | this.m_editor = monaco.editor.create(this.m_editorElem, {
46 | language: "json",
47 | model: this.m_model,
48 | });
49 |
50 | window.addEventListener("resize", () => this.resize());
51 | window.addEventListener("message", (data) => {
52 | if (
53 | !data.isTrusted ||
54 | data.origin !== window.location.origin ||
55 | data.target === data.source
56 | ) {
57 | return;
58 | }
59 | this.onMessage(data.data);
60 | });
61 |
62 | monaco.languages.registerHoverProvider("json", {
63 | provideHover: (model, position) => {
64 | const text = model.getLineContent(position.lineNumber);
65 |
66 | if (!text.includes('"when"')) {
67 | this.sendMsg({ command: "HighlightFeature", condition: "" });
68 | this.m_decorations = model.deltaDecorations(this.m_decorations, []);
69 | return;
70 | }
71 |
72 | try {
73 | if (text.indexOf('"when"') !== -1) {
74 | this.m_decorations = model.deltaDecorations(this.m_decorations, [
75 | {
76 | range: new monaco.Range(
77 | position.lineNumber,
78 | 0,
79 | position.lineNumber,
80 | text.length
81 | ),
82 | options: {
83 | isWholeLine: true,
84 | inlineClassName: "highlightedLine",
85 | },
86 | },
87 | ]);
88 | const lineText = text.split(":")[1].trim().replace(/"/g, "");
89 | this.sendMsg({ command: "HighlightFeature", condition: lineText });
90 | }
91 | } catch (err) {
92 | // nothing here
93 | }
94 | return null;
95 | },
96 | });
97 |
98 | // Inform the Theme editor that the text editor is ready. The Theme editor then will send a
99 | // "InitData" or "SetSourceValue" command.
100 | this.sendMsg({ command: "Init" });
101 |
102 | // When the theme source code is changing sends the changes to the Theme editor
103 | this.m_editor.onDidChangeModelContent(
104 | // Prevents too frequent code source updating
105 | throttle(1000, (event: monaco.editor.IModelContentChangedEvent) => {
106 | const code = this.m_editor!.getValue();
107 | const change = event.changes[event.changes.length - 1];
108 |
109 | this.lintWhenProperties(code);
110 |
111 | this.sendMsg({
112 | command: "UpdateSourceValue",
113 | line: change.range.startLineNumber,
114 | column: change.range.startColumn,
115 | value: code,
116 | });
117 | })
118 | );
119 |
120 | // When cursor position is changing sends the changes to the Theme editor
121 | this.m_editor.onDidChangeCursorPosition(
122 | // Prevents too frequent code source updating
123 | throttle(1000, (event: monaco.editor.ICursorPositionChangedEvent) => {
124 | this.sendMsg({
125 | command: "UpdateCursorPosition",
126 | line: event.position.lineNumber,
127 | column: event.position.column,
128 | });
129 | })
130 | );
131 |
132 | // sent notifications and error messages about current theme file
133 | setInterval(() => {
134 | this.m_monacoNotifications = monaco.editor
135 | .getModelMarkers({ take: 500 })
136 | .sort((a, b) => b.severity - a.severity);
137 | this.emit("updateNotifications", this.m_monacoNotifications);
138 |
139 | if (this.m_monacoNotifications.length === 0) {
140 | this.sendMsg({
141 | command: "UpdateNotificationsCount",
142 | count: 0,
143 | severity: 0,
144 | });
145 | } else {
146 | this.sendMsg({
147 | command: "UpdateNotificationsCount",
148 | count: this.m_monacoNotifications.length,
149 | severity: this.m_monacoNotifications[0].severity,
150 | });
151 | }
152 | }, 500);
153 |
154 | this.m_editor.focus();
155 | }
156 |
157 | resize() {
158 | if (this.m_editor === null) {
159 | throw new Error();
160 | }
161 | this.m_editor.layout();
162 | }
163 |
164 | updateMessagesSize(UpdateNotificationsSize: number) {
165 | this.sendMsg({ command: "UpdateNotificationsSize", UpdateNotificationsSize });
166 | }
167 |
168 | on(event: Commands | "updateNotifications", listener: (...args: any[]) => void) {
169 | return super.on(event, listener);
170 | }
171 |
172 | setCursor(lineNumber: number, column: number) {
173 | const cursorPosition = { lineNumber, column };
174 | this.m_editor!.setPosition(cursorPosition);
175 | this.m_editor!.revealPositionInCenter(cursorPosition);
176 | }
177 |
178 | /**
179 | * Finding wrong "when" properties in styles of theme
180 | */
181 | private lintWhenProperties(code: string) {
182 | let markers: monaco.editor.IMarker[] = [];
183 | try {
184 | const data = JSON.parse(code);
185 | const lines = code.split("\n");
186 |
187 | markers = Object.values(data.styles as { [key: string]: Style[] })
188 | // flatten all styles
189 | .reduce((a, b) => [...a, ...b], [])
190 | // find "when" props with errors
191 | .map((style) => {
192 | if (typeof style.when === "string") {
193 | try {
194 | Expr.parse(style.when);
195 | } catch (error) {
196 | return [style.when, error.message];
197 | }
198 | }
199 | return undefined;
200 | })
201 | .filter((query) => query !== undefined)
202 | // create Markers from errors
203 | .map((query) => {
204 | const startLineNumber = lines.findIndex((line) =>
205 | line.includes((query as string[])[0])
206 | );
207 | const startColumn = lines[startLineNumber].indexOf((query as string[])[0]);
208 | const result: monaco.editor.IMarker = {
209 | startLineNumber: startLineNumber + 1,
210 | endLineNumber: startLineNumber + 1,
211 | startColumn: startColumn + 1,
212 | endColumn: startColumn + 1 + (query as string[])[0].length,
213 | severity: 8,
214 | message: (query as string[])[1],
215 | owner: "editor-lint",
216 | resource: this.m_modelUri,
217 | };
218 | return result;
219 | });
220 | } catch (error) {
221 | /* */
222 | }
223 |
224 | monaco.editor!.setModelMarkers(this.m_model, "editor-lint", markers);
225 | }
226 |
227 | /**
228 | * Handle incoming messages from the parent window/page (editor).
229 | */
230 | private onMessage(msg: WindowCommands) {
231 | switch (msg.command) {
232 | case "InitData":
233 | const position = { lineNumber: msg.line, column: msg.column };
234 | this.m_editor!.setValue(msg.value);
235 | this.m_editor!.setPosition(position);
236 | this.m_editor!.revealPositionInCenter(position);
237 | break;
238 | case "SetCursor":
239 | this.setCursor(msg.line, msg.column);
240 | break;
241 | case "SetSourceValue":
242 | this.m_editor!.setValue(msg.value);
243 | break;
244 | case "GetSourceValue":
245 | this.sendMsg({ command: "SetSourceValue", value: this.m_editor!.getValue() });
246 | break;
247 | case "Format":
248 | this.m_editor!.getAction("editor.action.formatDocument").run();
249 | break;
250 | case "ShowCommands":
251 | this.m_editor!.getAction("editor.action.quickCommand").run();
252 | break;
253 | case "undo":
254 | this.m_editor!.trigger("aaaa", "undo", "aaaa");
255 | break;
256 | case "redo":
257 | this.m_editor!.trigger("aaaa", "redo", "aaaa");
258 | break;
259 | }
260 | this.emit(msg.command, msg);
261 | }
262 |
263 | /**
264 | * Send the message to the parent window/page (editor)
265 | */
266 | private sendMsg(msg: WindowCommands) {
267 | window.parent.postMessage(msg, window.location.origin);
268 | }
269 |
270 | get htmlElement() {
271 | return this.m_editorElem;
272 | }
273 | }
274 |
275 | export default new TextEditor();
276 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/src/map-editor/map-handler/index.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2017-2020 HERE Europe B.V.
3 | * Licensed under Apache 2.0, see full license in LICENSE
4 | * SPDX-License-Identifier: Apache-2.0
5 | */
6 | import { BaseStyle, Style, Theme } from "@here/harp-datasource-protocol";
7 | import { MapControls } from "@here/harp-map-controls";
8 | import {
9 | CopyrightElementHandler,
10 | CopyrightInfo,
11 | MapView,
12 | MapViewEventNames,
13 | MapViewUtils,
14 | } from "@here/harp-mapview";
15 | import { LoggerManager } from "@here/harp-utils";
16 | import { APIFormat, VectorTileDataSource } from "@here/harp-vectortile-datasource";
17 | import { VectorTileDecoder } from "@here/harp-vectortile-datasource/lib/VectorTileDecoder";
18 | import { EventEmitter } from "events";
19 | import { throttle } from "throttle-debounce";
20 | import { WhenPropsData } from "../../types";
21 | import { accessToken } from "../config";
22 | import settings from "../Settings";
23 | import textEditor from "../TextEditor";
24 | import { getGeometryData } from "./MapGeometryList";
25 | import MapViewState from "./MapViewState";
26 |
27 | export const logger = LoggerManager.instance.create("MapHandler");
28 | type Events = "init" | "mapRemoved" | "mapCreated";
29 |
30 | type AddStyleTechniqueResult = "err" | "ok" | "exists";
31 |
32 | /**
33 | * Manages the Map.
34 | */
35 | class MapHandler extends EventEmitter {
36 | get elem() {
37 | return this.m_canvasElem;
38 | }
39 | get copyrightElem() {
40 | return this.m_copyrightElem;
41 | }
42 |
43 | get controls() {
44 | return this.m_controls;
45 | }
46 |
47 | get state() {
48 | return this.m_mapViewState;
49 | }
50 |
51 | get mapView() {
52 | return this.m_mapView;
53 | }
54 |
55 | private m_canvasElem: HTMLCanvasElement | null = null;
56 | private m_copyrightElem: HTMLDivElement | null = null;
57 | /**
58 | * The map instance
59 | */
60 | private m_mapView: MapView | null = null;
61 | private m_controls: MapControls | null = null;
62 | /**
63 | * Represents the position and orientation of the camera.
64 | */
65 | private m_mapViewState: MapViewState = new MapViewState();
66 | /**
67 | * The current data source for the camera.
68 | */
69 | private m_datasource: VectorTileDataSource | null = null;
70 | private m_copyrights: CopyrightInfo[];
71 | private m_copyrightHandler: CopyrightElementHandler | null = null;
72 |
73 | /**
74 | * Removes the old map and create a new map with current theme.
75 | */
76 | private rebuildMap: () => void;
77 |
78 | /**
79 | * Callback for [[MapView]] [[MapViewEventNames.Render]] event that saves the current state of
80 | * map camera;
81 | */
82 | private onMapRender: () => void;
83 |
84 | /**
85 | * Notifying the editor that the camera position is changed.
86 | */
87 | private emitStateUpdate: () => void;
88 |
89 | private onMovementFinished: () => void;
90 |
91 | constructor() {
92 | super();
93 |
94 | const hereCopyrightInfo: CopyrightInfo = {
95 | id: "here.com",
96 | year: new Date().getFullYear(),
97 | label: "HERE",
98 | link: "https://legal.here.com/terms",
99 | };
100 |
101 | this.m_copyrights = [hereCopyrightInfo];
102 |
103 | this.onMapRender = () => {
104 | if (this.m_mapView === null || this.m_controls === null) {
105 | return;
106 | }
107 | const targetWorld = MapViewUtils.rayCastWorldCoordinates(this.m_mapView, 0, 0);
108 | const target = this.m_mapView.projection.unprojectPoint(targetWorld!);
109 | const state = new MapViewState(
110 | this.m_mapView.targetDistance,
111 | target,
112 | -this.m_controls.attitude.yaw,
113 | this.m_controls.attitude.pitch
114 | );
115 | this.m_mapViewState = state;
116 | this.emitStateUpdate();
117 | };
118 |
119 | this.rebuildMap = () => {
120 | if (this.m_canvasElem === null || this.m_copyrightElem === null) {
121 | return;
122 | }
123 |
124 | if (this.m_datasource !== null && this.m_mapView !== null) {
125 | this.m_mapView.removeDataSource(this.m_datasource);
126 | this.m_datasource = null;
127 | }
128 |
129 | if (this.m_mapView !== null) {
130 | this.m_mapView.removeEventListener(MapViewEventNames.Render, this.onMapRender);
131 | this.m_mapView.removeEventListener(
132 | MapViewEventNames.MovementFinished,
133 | this.onMovementFinished
134 | );
135 |
136 | this.m_mapView.dispose();
137 | this.m_mapView = null;
138 | }
139 |
140 | if (this.m_controls !== null) {
141 | this.m_controls.dispose();
142 | this.m_controls = null;
143 | }
144 |
145 | if (this.m_copyrightHandler !== null) {
146 | this.m_copyrightHandler.destroy();
147 | this.m_copyrightHandler = null;
148 | }
149 |
150 | this.emit("mapRemoved");
151 |
152 | const style = settings.get("editorCurrentStyle");
153 |
154 | let theme;
155 | try {
156 | const src = textEditor.getValue();
157 | theme = JSON.parse(src) as Theme;
158 | } catch (err) {
159 | logger.error(err);
160 | }
161 |
162 | this.m_mapView = new MapView({
163 | canvas: this.m_canvasElem,
164 | decoderUrl: "decoder.bundle.js",
165 | theme,
166 | });
167 |
168 | this.m_controls = new MapControls(this.m_mapView);
169 | this.m_controls.enabled = true;
170 |
171 | this.m_mapView.lookAt(
172 | this.m_mapViewState.target,
173 | this.m_mapViewState.distance,
174 | this.m_mapViewState.tilt,
175 | this.m_mapViewState.azimuth
176 | );
177 |
178 | this.m_datasource = new VectorTileDataSource({
179 | baseUrl: "https://xyz.api.here.com/tiles/herebase.02",
180 | apiFormat: APIFormat.XYZOMV,
181 | styleSetName: style || undefined,
182 | maxDisplayLevel: 17,
183 | authenticationCode: accessToken,
184 | copyrightInfo: this.m_copyrights,
185 | decoder: new VectorTileDecoder(),
186 | });
187 |
188 | this.m_copyrightHandler = CopyrightElementHandler.install(
189 | this.m_copyrightElem,
190 | this.m_mapView
191 | );
192 |
193 | this.m_mapView.addEventListener(
194 | MapViewEventNames.MovementFinished,
195 | this.onMovementFinished
196 | );
197 |
198 | this.m_mapView.addDataSource(this.m_datasource);
199 | this.m_mapView.addEventListener(MapViewEventNames.Render, this.onMapRender);
200 |
201 | this.emit("mapCreated");
202 | };
203 |
204 | this.emitStateUpdate = throttle(500, () => {
205 | settings.set("editorMapViewState", this.m_mapViewState.toString());
206 | });
207 |
208 | this.onMovementFinished = () => {
209 | getGeometryData(this.m_mapView as MapView, this.m_datasource as VectorTileDataSource);
210 | };
211 | }
212 |
213 | /**
214 | * Initialize MapHandler when the page is ready.
215 | */
216 | init(canvas: HTMLCanvasElement, copyrightElem: HTMLDivElement) {
217 | this.m_canvasElem = canvas;
218 | this.m_copyrightElem = copyrightElem;
219 | this.m_mapViewState = MapViewState.fromString(settings.get("editorMapViewState"));
220 |
221 | settings.on("setting:textEditor:sourceCode", this.rebuildMap);
222 | settings.on("setting:editorCurrentStyle", this.rebuildMap);
223 |
224 | this.rebuildMap();
225 |
226 | this.emit("init");
227 | }
228 |
229 | /**
230 | * Return userData of the clicked element;
231 | */
232 | intersect(event: MouseEvent) {
233 | if (this.m_canvasElem === null || this.m_mapView === null) {
234 | return null;
235 | }
236 | const rect = this.m_canvasElem.getBoundingClientRect();
237 | const x = event.clientX - rect.left;
238 | const y = event.clientY - rect.top;
239 |
240 | const intersectionResults = this.m_mapView.intersectMapObjects(x, y);
241 | if (!intersectionResults || intersectionResults.length === 0) {
242 | return null;
243 | }
244 |
245 | return intersectionResults[0].intersection!.object.userData;
246 | }
247 |
248 | resize() {
249 | if (
250 | this.m_mapView === null ||
251 | this.m_canvasElem === null ||
252 | this.m_canvasElem.parentElement === null
253 | ) {
254 | return;
255 | }
256 |
257 | const rect = this.m_canvasElem.parentElement.getBoundingClientRect();
258 |
259 | this.m_mapView.resize(rect.width, rect.height);
260 | }
261 |
262 | whenFromKeyVal(data: WhenPropsData) {
263 | const keys = ["$geometryType", "$layer", "kind", "kind_detail", "network"];
264 |
265 | return Object.entries(data)
266 | .map(([key, val]) => {
267 | if (!keys.includes(key)) {
268 | return;
269 | }
270 | return typeof val === "string" ? `${key} == '${val}'` : `${key} == ${val}`;
271 | })
272 | .filter((item) => item !== undefined)
273 | .join(" && ");
274 | }
275 |
276 | addStyleTechnique(style: Style): AddStyleTechniqueResult {
277 | const theme = textEditor.getParsedTheme();
278 | const currentStyle = settings.get("editorCurrentStyle");
279 |
280 | if (
281 | theme === null ||
282 | currentStyle === null ||
283 | theme.styles === undefined ||
284 | style.description === undefined
285 | ) {
286 | return "err";
287 | }
288 |
289 | const currentStyleSet = theme.styles[currentStyle];
290 |
291 | const descriptionIsExist = currentStyleSet.some(
292 | (item) => (item as BaseStyle).description === style.description
293 | );
294 |
295 | if (descriptionIsExist) {
296 | return "exists";
297 | }
298 |
299 | currentStyleSet.push(style);
300 |
301 | const source = JSON.stringify(theme, undefined, 4);
302 | textEditor.setValue(source);
303 |
304 | const descriptionKey = `"${currentStyle}"`;
305 | const index = source.indexOf(`"${style.description}"`, source.indexOf(descriptionKey));
306 |
307 | if (index > 0) {
308 | const lines = source.slice(0, index).split("\n");
309 | const symbols = lines[lines.length - 1].slice(
310 | lines[lines.length - 1].indexOf(descriptionKey)
311 | );
312 | textEditor.setCursor(lines.length, symbols.length);
313 | }
314 |
315 | return "ok";
316 | }
317 |
318 | emit(event: Events, ...args: any[]) {
319 | return super.emit(event, ...args);
320 | }
321 |
322 | on(event: Events, listener: (...args: any[]) => void): this {
323 | return super.on(event, listener);
324 | }
325 |
326 | once(event: Events, listener: (...args: any[]) => void): this {
327 | return super.once(event, listener);
328 | }
329 |
330 | removeListener(event: Events, listener: (...args: any[]) => void): this {
331 | return super.removeListener(event, listener);
332 | }
333 | }
334 |
335 | export default new MapHandler();
336 |
--------------------------------------------------------------------------------
/src/map-editor/TextEditor.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2017-2020 HERE Europe B.V.
3 | * Licensed under Apache 2.0, see full license in LICENSE
4 | * SPDX-License-Identifier: Apache-2.0
5 | */
6 | import { Theme } from "@here/harp-datasource-protocol";
7 | import { LoggerManager } from "@here/harp-utils";
8 | import * as React from "react";
9 | import { Side, WindowCommands } from "../types";
10 | import PopupsContainer from "./components-smart/PopupsContainer";
11 | import MapHighlighter from "./map-handler/MapHighliter";
12 | import settings from "./Settings";
13 |
14 | export const logger = LoggerManager.instance.create("TextEditor");
15 |
16 | /**
17 | * This class controls the monaco editor and communicate thru messages with the Theme editor.
18 | */
19 | class TextEditor {
20 | readonly elemEditor = document.createElement("div");
21 |
22 | private m_frameURL: string;
23 | /**
24 | * Contains the child window when [[Side.DeTouch]] is selected
25 | */
26 | private m_editorWindow: Window | null = null;
27 | /**
28 | * Contains the child page when [[Side.DeTouch]] is not selected
29 | */
30 | private m_editorIframe: null | HTMLIFrameElement = null;
31 | /**
32 | * The source code of current editable theme.
33 | */
34 | private m_value = "";
35 | /**
36 | * Last parsed source code of the theme. If equals [[null]] then the source code probably
37 | * invalid [[JSON]].
38 | */
39 | private m_parsedTheme: Theme | null = null;
40 |
41 | /**
42 | * handles commands of the child text editor
43 | */
44 | private onMessage: (data: MessageEvent) => void;
45 |
46 | constructor() {
47 | const pagePath = window.location.pathname.toLocaleLowerCase().replace("/index.html", "");
48 | this.m_frameURL = `${window.location.origin}/${pagePath}/textEditor.html`;
49 |
50 | this.elemEditor.id = "editor-container";
51 |
52 | this.onMessage = (data: MessageEvent) => {
53 | if (!data.isTrusted || data.origin !== window.location.origin) {
54 | return;
55 | }
56 |
57 | const msg: WindowCommands = data.data;
58 |
59 | switch (msg.command) {
60 | case "Init":
61 | this.sendMsg({
62 | command: "InitData",
63 | value: this.m_value,
64 | column: settings.get("textEditor:column"),
65 | line: settings.get("textEditor:line"),
66 | notificationsVisible: settings.get("notificationsVisible"),
67 | notificationsSize: settings.get("notificationsSize"),
68 | });
69 | break;
70 | case "HighlightFeature":
71 | MapHighlighter.highlight(msg.condition);
72 | break;
73 | case "UpdateSourceValue":
74 | this.updateSource(msg.value);
75 | settings.set("textEditor:sourceCode", this.m_value);
76 | settings.set("textEditor:column", msg.column);
77 | settings.set("textEditor:line", msg.line);
78 | break;
79 | case "UpdateCursorPosition":
80 | settings.set("textEditor:column", msg.column);
81 | settings.set("textEditor:line", msg.line);
82 | break;
83 | case "UpdateNotificationsCount":
84 | settings.setStoreData("notificationsState", {
85 | count: msg.count,
86 | severity: msg.severity,
87 | });
88 | break;
89 | case "UpdateNotificationsSize":
90 | settings.set("notificationsSize", msg.UpdateNotificationsSize);
91 | break;
92 | default:
93 | logger.warn(`unhandled command: ${msg.command}`);
94 | }
95 | };
96 | }
97 |
98 | async init() {
99 | this.createIframe();
100 | this.updateSource(settings.get("textEditor:sourceCode"));
101 |
102 | window.addEventListener("message", this.onMessage);
103 | window.addEventListener("beforeunload", () => {
104 | if (this.m_editorWindow !== null) {
105 | this.m_editorWindow.close();
106 | }
107 | });
108 |
109 | settings.on("setting:notificationsVisible", (notificationsVisible) => {
110 | this.sendMsg({
111 | command: "ToggleNotifications",
112 | notificationsVisible,
113 | notificationsSize: settings.get("notificationsSize"),
114 | });
115 | });
116 | }
117 |
118 | /**
119 | * Ensures that the text editor in the child iframe.
120 | */
121 | createIframe() {
122 | if (this.m_editorWindow !== null) {
123 | this.m_editorWindow.close();
124 | this.m_editorWindow = null;
125 | }
126 | if (this.m_editorIframe !== null) {
127 | return;
128 | }
129 |
130 | this.m_editorIframe = document.createElement("iframe");
131 | this.m_editorIframe.className = "editor";
132 | this.m_editorIframe.src = this.m_frameURL;
133 |
134 | this.elemEditor.appendChild(this.m_editorIframe);
135 | return this.m_editorIframe;
136 | }
137 |
138 | /**
139 | * Ensures that the text editor in the floating window.
140 | */
141 | createWindow() {
142 | if (this.m_editorWindow !== null) {
143 | return;
144 | }
145 |
146 | if (this.m_editorIframe !== null) {
147 | this.elemEditor.removeChild(this.m_editorIframe);
148 | this.m_editorIframe = null;
149 | }
150 |
151 | this.m_editorWindow = window.open(
152 | this.m_frameURL,
153 | "Text editor",
154 | "width=600,height=400,toolbar=0,status=0"
155 | );
156 |
157 | this.m_editorWindow!.addEventListener("message", this.onMessage);
158 |
159 | this.m_editorWindow!.onbeforeunload = () => {
160 | settings.emit("editor:setSide", Side.Left);
161 | };
162 | }
163 |
164 | /**
165 | * Theme source code getter.
166 | */
167 | getValue() {
168 | return this.m_value;
169 | }
170 |
171 | undo() {
172 | return this.sendMsg({ command: "undo" });
173 | }
174 |
175 | redo() {
176 | return this.sendMsg({ command: "redo" });
177 | }
178 |
179 | /**
180 | * Sets the source code, updates the available styles, and sets the proper style.
181 | */
182 | setValue(str: string) {
183 | this.sendMsg({ command: "SetSourceValue", value: str });
184 | this.updateSource(str);
185 | }
186 |
187 | /**
188 | * Send [[WindowCommands]] to the child text editor.
189 | */
190 | sendMsg(msg: WindowCommands) {
191 | if (this.m_editorWindow !== null) {
192 | this.m_editorWindow.postMessage(msg, this.m_frameURL);
193 | } else if (this.m_editorIframe !== null && this.m_editorIframe.contentWindow !== null) {
194 | this.m_editorIframe.contentWindow.postMessage(msg, this.m_frameURL);
195 | }
196 | }
197 |
198 | /**
199 | * Generates a file and open a file save dialogue.
200 | */
201 | download() {
202 | saveData(this.getValue(), "theme.json");
203 | }
204 |
205 | /**
206 | * Opens a file and sets it as the source code of the theme.
207 | */
208 | openFile() {
209 | openFile()
210 | .then((value) => {
211 | this.setValue(value);
212 | })
213 | .catch(() => {
214 | const popup = {
215 | name: "ERROR",
216 | options: {},
217 | component: Can't open file.
,
218 | };
219 | PopupsContainer.addPopup(popup);
220 | });
221 | }
222 |
223 | /**
224 | * Send a [[Format]] command to the child text editor.
225 | */
226 | formatFile() {
227 | this.sendMsg({ command: "Format" });
228 | }
229 |
230 | /**
231 | * Send a [[ShowCommands]] command to the child text editor.
232 | */
233 | showCommands() {
234 | this.sendMsg({ command: "ShowCommands" });
235 | }
236 |
237 | getParsedTheme() {
238 | return this.m_parsedTheme;
239 | }
240 |
241 | setCursor(line: number, column: number) {
242 | this.sendMsg({
243 | command: "SetCursor",
244 | column,
245 | line,
246 | });
247 | }
248 |
249 | /**
250 | * Updates the available styles list, and sets the proper style.
251 | */
252 | private updateSource(source: string) {
253 | this.m_value = source;
254 | this.m_parsedTheme = null;
255 | let styles: string[] = [];
256 |
257 | try {
258 | this.m_parsedTheme = JSON.parse(source) as Theme;
259 | } catch (error) {
260 | settings.setStoreData("styles", styles);
261 | settings.setStoreData("parsedTheme", this.m_parsedTheme);
262 | return;
263 | }
264 |
265 | if (this.m_parsedTheme.styles !== undefined) {
266 | const values = Object.values(this.m_parsedTheme.styles);
267 | if (values.length > 0 && values.every((value) => Array.isArray(value))) {
268 | styles = Object.keys(this.m_parsedTheme.styles);
269 | }
270 | }
271 |
272 | const currentStyle = settings.get("editorCurrentStyle");
273 |
274 | settings.setStoreData("styles", styles);
275 | settings.setStoreData("parsedTheme", this.m_parsedTheme);
276 |
277 | if (styles.length === 0) {
278 | settings.set("editorCurrentStyle", null);
279 | } else if (currentStyle === null || !styles.includes(currentStyle)) {
280 | settings.set("editorCurrentStyle", styles[0]);
281 | }
282 | }
283 | }
284 |
285 | const saveData = (() => {
286 | const link = document.createElement("a");
287 | document.body.appendChild(link);
288 | link.style.display = "none";
289 | return (data: string, fileName: string) => {
290 | const blob = new Blob([data], { type: "octet/stream" });
291 | const url = window.URL.createObjectURL(blob);
292 | link.href = url;
293 | link.download = fileName;
294 | link.click();
295 | window.URL.revokeObjectURL(url);
296 | };
297 | })();
298 |
299 | async function openFile(): Promise {
300 | const fileBrowser = document.createElement("input");
301 | document.body.appendChild(fileBrowser);
302 | fileBrowser.style.display = "none";
303 | fileBrowser.type = "file";
304 |
305 | fileBrowser.click();
306 |
307 | function removeObj() {
308 | document.body.removeChild(fileBrowser);
309 | }
310 |
311 | return new Promise((resolve, reject) => {
312 | fileBrowser.addEventListener(
313 | "change",
314 | (event) => {
315 | if (!event || !event.target) {
316 | return reject("");
317 | }
318 |
319 | const { files } = event.target as HTMLInputElement;
320 |
321 | if (!files || !files[0]) {
322 | return reject("");
323 | }
324 |
325 | const reader = new FileReader();
326 | reader.onload = () => {
327 | resolve((reader.result || "").toString());
328 | };
329 | reader.onerror = () => {
330 | reject("");
331 | };
332 | reader.readAsText(files[0]);
333 | },
334 | false
335 | );
336 | }).then((str) => {
337 | removeObj();
338 | return str as string;
339 | });
340 | }
341 |
342 | export default new TextEditor();
343 |
--------------------------------------------------------------------------------
/src/style.scss:
--------------------------------------------------------------------------------
1 | $bg-color: #333;
2 | $color-error: #F55;
3 | $color-ok: #5F5;
4 | $color-warn: #FF5;
5 | $bg-hover-color: #345;
6 | $icon-color: #fff;
7 | $icon-disabled: rgba($color: $icon-color, $alpha: 0.5);
8 | $icon-active: #77f;
9 | $icon-primary: rgb(34, 101, 179);
10 | $step: 32px;
11 | $separator-size: $step * 0.25 * 0.5;
12 | $font-size: $step * 0.5;
13 | $notifier-height: $step * 4;
14 |
15 |
16 | *>* {
17 | border: 0px;
18 | margin: 0px;
19 | padding: 0px;
20 | box-sizing: border-box;
21 | }
22 |
23 | body,
24 | html,
25 | #root,
26 | #app,
27 | #editor,
28 | #editor-container {
29 | width: 100%;
30 | height: 100%;
31 | color: $icon-color;
32 | }
33 |
34 | body {
35 | overflow: hidden;
36 | }
37 |
38 | .text-button {
39 | cursor: pointer;
40 | text-decoration: underline;
41 | }
42 |
43 | #root {
44 | background-color: $bg-hover-color;
45 | color: $icon-color;
46 | font-family: monospace;
47 | font-size: $font-size;
48 |
49 | .list {
50 | list-style: none;
51 |
52 | li {
53 | padding-left: $step * 0.5;
54 | }
55 | }
56 |
57 | .select-list {
58 | list-style: none;
59 |
60 | li {
61 | .text-button:hover {
62 | text-decoration: underline;
63 | }
64 |
65 | .text-button {
66 | text-decoration: none;
67 | }
68 | }
69 |
70 | li::before {
71 | content: "▸ ";
72 | }
73 | }
74 |
75 |
76 | a {
77 | color: $icon-color;
78 | }
79 |
80 | p {
81 | margin: $separator-size 0;
82 | }
83 |
84 | .popup-input {
85 | font-size: $step * 0.75;
86 | font-size: $step * 0.25;
87 | color: $bg-color;
88 | background-color: $icon-color;
89 | font-size: $step * 0.75;
90 | margin-top: $step * 0.5;
91 | width: 100%;
92 | }
93 |
94 | .popup-textarea {
95 | @extend .popup-input;
96 | height: $step * 6;
97 | height: $step * 8;
98 | }
99 |
100 | }
101 |
102 |
103 | #text-editor {
104 | position: absolute;
105 | width: 100%;
106 | height: 100%;
107 | background-color: $icon-color;
108 | top: auto;
109 | left: auto;
110 | bottom: auto;
111 | right: auto;
112 |
113 | #editor-container {
114 | overflow: hidden;
115 |
116 | .editor {
117 | width: 100%;
118 | height: 100%;
119 | }
120 | }
121 | }
122 |
123 | #menu {
124 | position: absolute;
125 | left: auto;
126 | bottom: auto;
127 | top: auto;
128 | right: auto;
129 | background-color: $bg-color;
130 | padding: $separator-size;
131 | }
132 |
133 |
134 | #text-editor.left {
135 | left: 0;
136 | top: 0;
137 | }
138 |
139 | #text-editor.right {
140 | right: 0;
141 | top: 0;
142 | }
143 |
144 | #text-editor.top {
145 | left: 0;
146 | top: 0;
147 | }
148 |
149 | #text-editor.bottom {
150 | left: 0;
151 | bottom: 0;
152 | }
153 |
154 | #text-editor.top,
155 | #text-editor.bottom {
156 | .button-icon+.button-icon {
157 | margin-left: $separator-size;
158 | }
159 | }
160 |
161 | #text-editor.left,
162 | #text-editor.right {
163 | .button-icon+.button-icon {
164 | margin-top: $separator-size;
165 | }
166 | }
167 |
168 | #map-container {
169 | height: 100%;
170 | width: 100%;
171 |
172 | .map {
173 | height: 100%;
174 | width: 100%;
175 | }
176 |
177 | #info-block {
178 | position: absolute;
179 | bottom: auto;
180 | right: auto;
181 | top: auto;
182 | left: auto;
183 | color: $icon-color;
184 | white-space: pre;
185 | height: fit-content;
186 |
187 | #intersect-info {
188 | background-color: $bg-color;
189 | padding: $separator-size * 2;
190 | font-weight: bold;
191 | }
192 | }
193 |
194 | #controls-container {
195 | position: absolute;
196 | left: auto;
197 | top: auto;
198 | right: auto;
199 | bottom: auto;
200 | }
201 |
202 | #copyright {
203 | background-color: $bg-hover-color;
204 | position: absolute;
205 | bottom: 0;
206 | right: 0;
207 | opacity: 0.5;
208 | padding: 2px 6px;
209 | }
210 |
211 | #copyright:hover {
212 | opacity: 1;
213 | background-color: $bg-color;
214 | transition: opacity 0.5s ease-in-out, background-color 0.5s ease-in-out;
215 | }
216 | }
217 |
218 | #map-container.bottom,
219 | #map-container.top {
220 | #menu .button-icon {
221 | float: left;
222 | }
223 | }
224 |
225 | #map-container.left>#menu {
226 | bottom: 0;
227 | left: -$separator-size;
228 | }
229 |
230 | #map-container.right>#menu {
231 | bottom: 0;
232 | right: -$separator-size;
233 | }
234 |
235 | #map-container.top>#menu {
236 | top: -$separator-size;
237 | left: 0;
238 | }
239 |
240 | #map-container.bottom>#menu {
241 | bottom: -$separator-size;
242 | left: 0;
243 | }
244 |
245 | #map-container.left>#menu.hidden {
246 | left: 0;
247 | }
248 |
249 | #map-container.right>#menu.hidden {
250 | right: 0;
251 | }
252 |
253 | #map-container.top>#menu.hidden {
254 | top: 0;
255 | }
256 |
257 | #map-container.bottom>#menu.hidden {
258 | bottom: 0;
259 | }
260 |
261 |
262 | #map-container.left,
263 | #map-container.top,
264 | #map-container.float,
265 | #map-container.bottom {
266 | #info-block {
267 | bottom: 0;
268 | right: 0;
269 | }
270 |
271 | #controls-container {
272 | top: 0;
273 | bottom: 0;
274 | right: 0;
275 | }
276 | }
277 |
278 | #map-container.bottom {
279 | #info-block {
280 | top: 0;
281 | right: 0;
282 | }
283 |
284 | #copyright {
285 | bottom: auto;
286 | top: 0;
287 | }
288 | }
289 |
290 | #map-container.right {
291 | #info-block {
292 | bottom: 0;
293 | left: 0;
294 | }
295 |
296 | #controls-container {
297 | top: 0;
298 | bottom: 0;
299 | left: 70px;
300 | }
301 |
302 | #copyright {
303 | bottom: 0;
304 | right: auto;
305 | left: 0;
306 | }
307 | }
308 |
309 | #mouse-catcher {
310 | position: fixed;
311 | top: 0;
312 | left: 0;
313 | right: 0;
314 | bottom: 0;
315 | }
316 |
317 | #mouse-catcher.left,
318 | #mouse-catcher.right {
319 | cursor: ew-resize;
320 | }
321 |
322 | #mouse-catcher.top,
323 | #mouse-catcher.bottom {
324 | cursor: ns-resize;
325 | }
326 |
327 | .button-icon.top {
328 | transform: rotate(90deg);
329 | }
330 |
331 | .button-icon.right {
332 | transform: rotate(180deg);
333 | }
334 |
335 | .button-icon.bottom {
336 | transform: rotate(270deg);
337 | }
338 |
339 | .button-icon {
340 | color: $icon-color;
341 | width: $step;
342 | height: $step;
343 | font-size: $step * 0.75;
344 | outline: none;
345 | padding: $step * 0.25 * 0.5;
346 | background-color: $bg-color;
347 | display: block;
348 | position: relative;
349 |
350 | span {
351 | color: $icon-color;
352 | position: absolute;
353 | bottom: 0;
354 | right: 0;
355 | font-size: 0.6em;
356 | background-color: rgba($color: $bg-color, $alpha: 0.7);
357 | border-radius: 4px;
358 | }
359 | }
360 |
361 | .button-icon.error {
362 | color: $color-error;
363 | }
364 |
365 | .button-icon.secondary {
366 | color: $icon-disabled;
367 | }
368 |
369 | .button-icon.primary {
370 | background-color: $icon-primary;
371 | }
372 |
373 | .button-icon.warn {
374 | color: $color-warn;
375 | }
376 |
377 | .icon {
378 | color: $icon-color;
379 | width: $step;
380 | height: $step;
381 | font-size: $step * 0.75;
382 | outline: none;
383 | padding: $step * 0.25 * 0.5;
384 | background-color: $bg-color;
385 | display: block;
386 | }
387 |
388 | .button-icon.small {
389 | color: $icon-color;
390 | font-size: $step * 0.5;
391 | outline: none;
392 | width: $step * 0.75;
393 | height: $step * 0.75;
394 | background-color: $bg-color;
395 | display: block;
396 | }
397 |
398 | .button-icon.active {
399 | background-color: $icon-active;
400 |
401 | span {
402 | background-color: $icon-active;
403 | }
404 | }
405 |
406 | .button-icon:active {
407 | color: $icon-active;
408 | }
409 |
410 |
411 | .button-icon.disabled {
412 | color: $icon-disabled;
413 | }
414 |
415 | .button-icon:hover {
416 | background-color: $bg-hover-color;
417 |
418 | span {
419 | background-color: $bg-hover-color;
420 | }
421 | }
422 |
423 | .button-icon.disabled:hover {
424 | background-color: $bg-color;
425 |
426 | span {
427 | background-color: $bg-color;
428 | }
429 | }
430 |
431 | .popup {
432 | position: fixed;
433 | width: 100%;
434 | height: 100%;
435 | left: 0;
436 | top: 0;
437 | background-color: rgba(0, 0, 0, 0.4);
438 |
439 | .window {
440 | position: relative;
441 | background-color: $bg-hover-color;
442 | margin: 0 auto;
443 | top: 50%;
444 | transform: translateY(-50%);
445 | width: max-content;
446 |
447 | header {
448 | background-color: $bg-color;
449 | text-align: center;
450 | font-size: $step * 0.5;
451 | padding: $step * 0.25;
452 | height: $step;
453 |
454 | .button-icon {
455 | position: absolute;
456 | top: 0;
457 | right: 0;
458 | }
459 | }
460 |
461 |
462 | >.content {
463 | padding: $step * 0.25;
464 | max-height: 80vh;
465 | max-width: 80vw;
466 | min-width: $step * 12;
467 | min-height: $step * 8;
468 | overflow: auto;
469 |
470 | .info {
471 | max-width: 800px;
472 | max-height: 600px;
473 | overflow: auto;
474 |
475 | section+section {
476 | margin-top: $step * 0.5;
477 | }
478 | }
479 | }
480 | }
481 |
482 | .window.close-button {
483 | header {
484 | padding-right: $step * 0.25 + $step;
485 | }
486 | }
487 | }
488 |
489 | .tabs {
490 | >ul {
491 | display: table;
492 | width: 100%;
493 | border-bottom: $bg-color $separator-size solid;
494 | user-select: none;
495 | margin-bottom: $separator-size;
496 |
497 | li {
498 | padding: $separator-size * 2 $step * 0.5 $separator-size;
499 | display: table-cell;
500 | text-align: center;
501 | }
502 |
503 | li:hover,
504 | li.active {
505 | background-color: $bg-color;
506 | }
507 |
508 | li.disabled:hover,
509 | li.disabled {
510 | color: $icon-disabled;
511 | background-color: transparent;
512 | }
513 | }
514 |
515 | .tab-content {
516 | padding: $step * 0.5;
517 | }
518 | }
519 |
520 | .no-select {
521 | user-select: none;
522 | -ms-user-select: none;
523 | -webkit-user-select: none;
524 | -moz-user-select: none;
525 | }
526 |
527 | .select {
528 | user-select: unset;
529 | -ms-user-select: unset;
530 | -webkit-user-select: unset;
531 | -moz-user-select: unset;
532 | }
533 |
534 | h3 {
535 | text-align: center;
536 | margin-top: $step * 0.5;
537 | margin-bottom: $step * 0.5;
538 | }
539 |
540 | #create-technique {
541 | input {
542 | margin-bottom: $step * 1.25;
543 | padding: $step * 0.1 $step * 0.25;
544 | }
545 |
546 | .button-icon {
547 | position: absolute;
548 | bottom: $step * 0.25;
549 | right: $step * 0.25;
550 | display: inline-block;
551 | }
552 | }
553 |
554 | .split-view {
555 | background-color: $bg-color;
556 | position: relative;
557 | overflow: hidden;
558 | widows: 100%;
559 | height: 100%;
560 |
561 | >section {
562 | position: absolute;
563 | overflow: hidden;
564 | top: 0;
565 | bottom: 0;
566 | left: 0;
567 | right: 0;
568 | background-color: $icon-color;
569 | }
570 |
571 | >.separator {
572 | background-color: $bg-color;
573 | position: absolute;
574 | bottom: 0;
575 | top: 0;
576 | left: 0;
577 | right: 0;
578 | }
579 |
580 | >.mouse-catcher {
581 | position: fixed;
582 | top: 0;
583 | left: 0;
584 | right: 0;
585 | bottom: 0;
586 | }
587 | }
588 |
589 | .split-view.horizontal {
590 |
591 | >.separator,
592 | >.mouse-catcher {
593 | cursor: ew-resize;
594 | }
595 | }
596 |
597 | .split-view.vertical {
598 |
599 | >.separator,
600 | >.mouse-catcher {
601 | cursor: ns-resize;
602 | }
603 | }
604 |
605 | #text-editor-container {
606 | width: 100%;
607 | height: 100%;
608 | position: relative;
609 |
610 | >div {
611 | height: 100%;
612 | }
613 | }
614 |
615 | .highlightedLine {
616 | background-color: rgb(169, 255, 119);
617 | }
618 |
619 | #notifications {
620 | height: 100%;
621 | background-color: $bg-color;
622 | padding: $separator-size;
623 | overflow: auto;
624 |
625 | ul {
626 | display: block;
627 | // height: $notifier-height - ($separator-size * 2);
628 |
629 | li.error {
630 | color: $color-error;
631 | }
632 | }
633 | }
634 |
635 | #popup-copy-link {
636 | .popup-input {
637 | width: calc(100% - (#{$step + $separator-size}));
638 | margin-top: 0;
639 | height: $step;
640 | }
641 |
642 | .button-icon {
643 | display: inline-block;
644 | vertical-align: sub;
645 | float: right;
646 | }
647 |
648 | clear: both;
649 | }
650 |
651 | #share-link-popup {
652 | .window {
653 | .content {
654 | min-height: auto;
655 | }
656 | }
657 | }
--------------------------------------------------------------------------------
/src/map-editor/datasourceSchemaModified.json:
--------------------------------------------------------------------------------
1 | {
2 | "water": {
3 | "geometry_types": ["point", "line", "polygon"],
4 | "properties": {
5 | "default": {
6 | "kind": "see below FIXME -->",
7 | "name": "This property contains the name of the line and includes localized name variants.",
106 | "min_zoom": "This property contains a suggested minimum zoom level at which the transit line should become visible.",
107 | "name": "This property contains the name of the line and includes localized name variants.",
108 | "id": "This is an ID used internally within HERE.