├── .gitignore ├── .prettierrc.json ├── .prettierignore ├── editor.png ├── .npmignore ├── .ort.yml ├── scripts └── publish-packages.sh ├── src ├── decoder.ts ├── map-editor │ ├── index.html │ ├── index.tsx │ ├── components-smart │ │ ├── App.tsx │ │ ├── PopupSelectLink.tsx │ │ ├── Info.tsx │ │ ├── PopupGeometriesList.tsx │ │ ├── Editor.tsx │ │ ├── PopupsContainer.tsx │ │ ├── MapElem.tsx │ │ ├── PopupSelectTheme.tsx │ │ ├── PopupCreateTechnique.tsx │ │ └── Menu.tsx │ ├── map-handler │ │ ├── MapViewState.ts │ │ ├── MapHighliter.ts │ │ ├── MapGeometryList.ts │ │ └── index.ts │ ├── components │ │ ├── SelectString.tsx │ │ ├── Tabs.tsx │ │ └── ButtonIcon.tsx │ ├── Component.ts │ ├── Settings.ts │ ├── TextEditor.tsx │ └── datasourceSchemaModified.json ├── text-editor-frame │ ├── textEditor.html │ ├── index.tsx │ ├── components-smart │ │ ├── TextEditorElem.tsx │ │ ├── App.tsx │ │ └── Notifications.tsx │ └── TextEditor.ts ├── components │ ├── TextButton.tsx │ └── SplitView.tsx ├── types.ts └── style.scss ├── typedoc.json ├── tsconfig.json ├── .travis.yml ├── tslint.json ├── package.json ├── webpack.config.js ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules/ 2 | dist/ 3 | yarn-error.log -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 4 4 | } 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | src/text-editor-frame/harp-theme.vscode.schema.json 3 | -------------------------------------------------------------------------------- /editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heremaps/harp-map-editor/HEAD/editor.png -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | test/ 4 | lib/ 5 | tsconfig.json 6 | webpack.config.ts 7 | .gitignore 8 | .gitreview 9 | *.tgz 10 | .rpt2_cache 11 | -------------------------------------------------------------------------------- /.ort.yml: -------------------------------------------------------------------------------- 1 | excludes: 2 | scopes: 3 | - name: "devDependencies" 4 | reason: "BUILD_TOOL_OF" 5 | comment: "These are dependencies only used for development. They are not distributed in the context of this product." 6 | -------------------------------------------------------------------------------- /scripts/publish-packages.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | # 4 | # Simple script that bundles the publishing of packages 5 | # to be run from Travis 6 | # 7 | 8 | echo '//registry.npmjs.org/:_authToken=${NPM_TOKEN}' > ~/.npmrc 9 | npm publish -------------------------------------------------------------------------------- /src/decoder.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017-2019 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 { VectorTileDecoderService } from "@here/harp-vectortile-datasource/index-worker"; 7 | 8 | VectorTileDecoderService.start(); 9 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "harp-map-editor", 3 | "mode": "file", 4 | "module": "commonjs", 5 | "out": "dist/doc", 6 | "exclude": ["**/node_modules/**"], 7 | "readme": "README.md", 8 | "target": "ES6", 9 | "excludePrivate": "true", 10 | "excludeExternals": "true" 11 | } 12 | -------------------------------------------------------------------------------- /src/map-editor/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | harp.gl Map Editor 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /src/text-editor-frame/textEditor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | HARP.gl Text Editor 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /src/text-editor-frame/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017-2019 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 * as ReactDOM from "react-dom"; 8 | import "../style.scss"; 9 | import App from "./components-smart/App"; 10 | 11 | ReactDOM.render(, document.getElementById("root")); 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "lib": ["es2017", "dom"], 6 | "sourceMap": true, 7 | "baseUrl": ".", 8 | "rootDir": ".", 9 | "outDir": "./dist/node_modules/", 10 | "jsx": "react", 11 | "jsxFactory": "React.createElement", 12 | "strict": true, 13 | "resolveJsonModule": true, 14 | "downlevelIteration": true, 15 | "newLine": "LF", 16 | "noImplicitReturns": true 17 | }, 18 | "exclude": ["node_modules", "dist"] 19 | } 20 | -------------------------------------------------------------------------------- /src/map-editor/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017-2019 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 * as ReactDOM from "react-dom"; 8 | import "../style.scss"; 9 | import App from "./components-smart/App"; 10 | import settings from "./Settings"; 11 | import textEditor from "./TextEditor"; 12 | 13 | settings 14 | .init() 15 | .then(() => textEditor.init()) 16 | .then(() => { 17 | ReactDOM.render(, document.getElementById("root")); 18 | }); 19 | -------------------------------------------------------------------------------- /src/map-editor/components-smart/App.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017-2019 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 Component, { SettingsState } from "../Component"; 8 | import Editor from "./Editor"; 9 | import PopupsContainer from "./PopupsContainer"; 10 | 11 | export default class App extends Component { 12 | render() { 13 | return ( 14 |
15 | 16 | 17 |
18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/components/TextButton.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017-2019 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 | interface Props { 9 | className?: string; 10 | onClick?: (event: React.MouseEvent) => void; 11 | } 12 | 13 | export default class extends React.Component { 14 | render() { 15 | const className = "text-button" + (this.props.className || ""); 16 | return ( 17 | 18 | {this.props.children} 19 | 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/text-editor-frame/components-smart/TextEditorElem.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 textEditor from "../TextEditor"; 8 | 9 | export default class App extends React.Component { 10 | private m_container: HTMLDivElement | null = null; 11 | 12 | componentDidMount() { 13 | if (this.m_container === null) { 14 | throw new Error(); 15 | } 16 | 17 | if (!textEditor.htmlElement) { 18 | textEditor.init(); 19 | } 20 | 21 | if (textEditor.htmlElement === null) { 22 | throw new Error(); 23 | } 24 | 25 | this.m_container.appendChild(textEditor.htmlElement); 26 | 27 | requestAnimationFrame(() => { 28 | textEditor.resize(); 29 | }); 30 | } 31 | 32 | render() { 33 | return
(this.m_container = node)} />; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/map-editor/map-handler/MapViewState.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017-2019 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 { GeoCoordinates } from "@here/harp-geoutils/lib/coordinates/GeoCoordinates"; 7 | 8 | const CURRENT_VERSION = 1; 9 | 10 | /** 11 | * Represents the position and orientation of the camera. Used for recreating the camera after page 12 | * reload. 13 | */ 14 | export default class MapViewState { 15 | static fromString(str: string) { 16 | const data = JSON.parse(str); 17 | 18 | if (data.version !== CURRENT_VERSION) { 19 | return new MapViewState(); 20 | } 21 | return new MapViewState(data.distance, data.target, data.azimuth, data.tilt); 22 | } 23 | 24 | readonly version = CURRENT_VERSION; 25 | constructor( 26 | public distance = 3000, 27 | public target = new GeoCoordinates(52.518611, 13.376111, 0), 28 | public azimuth = 0, 29 | public tilt = 0 30 | ) {} 31 | 32 | toString() { 33 | return JSON.stringify(this); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/map-editor/components/SelectString.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017-2019 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 | 9 | interface Props { 10 | values: T[]; 11 | active?: T; 12 | onSelect: (val: T) => void; 13 | } 14 | 15 | export default class SelectString extends React.Component> { 16 | render() { 17 | const { values, active } = this.props; 18 | 19 | return ( 20 |
    21 | {values.map((val, i) => { 22 | return ( 23 |
  • 24 | this.props.onSelect(val)} 26 | className={val === active ? "active" : ""} 27 | > 28 | {val} 29 | 30 |
  • 31 | ); 32 | })} 33 |
34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | dist: xenial 3 | node_js: 4 | - "10" 5 | cache: 6 | yarn: true 7 | 8 | addons: 9 | chrome: stable 10 | firefox: latest 11 | 12 | branches: 13 | only: 14 | - master 15 | - /^v\d+\.\d+(\.\d+)?(-\S*)?$/ 16 | 17 | # upgrade yarn to a more recent version 18 | before_install: 19 | - curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.13.0 20 | - export PATH="$HOME/.yarn/bin:$PATH" 21 | 22 | jobs: 23 | include: 24 | - name: "Test" 25 | script: | 26 | set -ex 27 | yarn run test 28 | yarn run tslint 29 | yarn run prettier 30 | - name: "Build & Deploy" 31 | script: | 32 | set -ex 33 | yarn run typedoc 34 | yarn run build 35 | deploy: 36 | - provider: script 37 | script: ./scripts/publish-packages.sh 38 | skip_cleanup: true 39 | on: 40 | tags: true 41 | branch: master 42 | - provider: pages 43 | skip_cleanup: true 44 | commiter-from-gh: true 45 | keep-history: false 46 | local-dir: dist 47 | github-token: $GITHUB_TOKEN 48 | on: 49 | tags: true 50 | branch: master 51 | 52 | -------------------------------------------------------------------------------- /src/map-editor/components-smart/PopupSelectLink.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017-2019 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 ButtonIcon, { ICONS } from "../components/ButtonIcon"; 8 | 9 | interface Props { 10 | link: string; 11 | done?: () => void; 12 | } 13 | 14 | function copyText(text: string) { 15 | function selectElementText(domElement: HTMLElement) { 16 | const range = document.createRange(); 17 | range.selectNode(domElement); 18 | const selection = window.getSelection(); 19 | if (selection !== null) { 20 | selection.removeAllRanges(); 21 | selection.addRange(range); 22 | } 23 | } 24 | const element = document.createElement("DIV"); 25 | element.textContent = text; 26 | document.body.appendChild(element); 27 | selectElementText(element); 28 | document.execCommand("copy"); 29 | element.remove(); 30 | } 31 | 32 | export default class extends React.Component { 33 | render() { 34 | return ( 35 | 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/map-editor/components/Tabs.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017-2019 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 | export interface Tab { 9 | name: string; 10 | component: JSX.Element | null; 11 | disabled?: boolean; 12 | } 13 | 14 | interface Props { 15 | tabs: Tab[]; 16 | active: Tab; 17 | id?: string; 18 | onChange: (tab: Tab) => void; 19 | } 20 | 21 | export default class extends React.Component { 22 | render() { 23 | if (this.props.tabs.length === 0) { 24 | return null; 25 | } else if (this.props.tabs.length === 1) { 26 | return this.props.tabs[0].component; 27 | } 28 | 29 | return ( 30 |
31 |
    32 | {this.props.tabs.map((tab, i) => { 33 | let classes = tab === this.props.active ? "active" : ""; 34 | classes += tab.disabled === true ? " disabled" : ""; 35 | 36 | return ( 37 |
  • { 40 | if (tab.disabled === true) { 41 | return; 42 | } 43 | this.props.onChange(tab); 44 | }} 45 | className={classes} 46 | > 47 | {tab.name} 48 |
  • 49 | ); 50 | })} 51 |
52 |
{this.props.active.component}
53 |
54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/text-editor-frame/components-smart/App.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 SplitView from "../../components/SplitView"; 8 | import textEditor from "../TextEditor"; 9 | import Notifications from "./Notifications"; 10 | import TextEditorElem from "./TextEditorElem"; 11 | 12 | interface State { 13 | notificationsVisible: boolean; 14 | notificationsSize: number; 15 | } 16 | 17 | export default class App extends React.Component { 18 | constructor(props: any) { 19 | super(props); 20 | this.state = { 21 | notificationsVisible: false, 22 | notificationsSize: 0, 23 | }; 24 | 25 | textEditor.on("InitData", ({ notificationsVisible, notificationsSize }) => { 26 | this.setState({ notificationsVisible, notificationsSize }); 27 | }); 28 | textEditor.on("ToggleNotifications", ({ notificationsVisible, notificationsSize }) => { 29 | this.setState({ notificationsVisible, notificationsSize }); 30 | }); 31 | } 32 | 33 | render() { 34 | let content = null; 35 | 36 | if (this.state.notificationsVisible) { 37 | content = ( 38 | } 41 | section_b={} 42 | mode="vertical" 43 | onChange={(size) => { 44 | textEditor.resize(); 45 | textEditor.updateMessagesSize(size); 46 | }} 47 | /> 48 | ); 49 | } else { 50 | content = ; 51 | } 52 | 53 | return
{content}
; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/map-editor/components-smart/Info.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 Component, { SettingsState } from "../Component"; 8 | import MapHandler from "../map-handler"; 9 | 10 | interface State extends SettingsState { 11 | intersectInfo: { [key: string]: any } | null; 12 | } 13 | 14 | export default class extends Component { 15 | constructor(props: {}) { 16 | super(props); 17 | this.state = { 18 | settings: {}, 19 | store: {}, 20 | intersectInfo: {}, 21 | }; 22 | } 23 | 24 | componentDidMount() { 25 | this.connectSettings(["editorTabVisible", "editorInfoPick"]); 26 | 27 | MapHandler.once("init", () => { 28 | MapHandler.elem!.addEventListener("click", (event: MouseEvent) => { 29 | if (!this.state.settings.editorInfoPick) { 30 | return; 31 | } 32 | const intersectInfo = MapHandler.intersect(event); 33 | this.setState({ intersectInfo }); 34 | event.preventDefault(); 35 | }); 36 | }); 37 | } 38 | 39 | componentWillUpdate() { 40 | if (!this.state.settings.editorInfoPick && this.state.intersectInfo) { 41 | this.setState({ intersectInfo: null }); 42 | } 43 | } 44 | 45 | render() { 46 | let IntersectInfo = null; 47 | if (this.state.intersectInfo) { 48 | IntersectInfo = ( 49 |
55 | ); 56 | } 57 | 58 | return
{IntersectInfo}
; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "comment-format": true, 5 | "completed-docs": false, 6 | "curly": [true, "all"], 7 | "indent": [true, "spaces", 4], 8 | "interface-name": false, 9 | "jsdoc-format": false, 10 | "linebreak-style": [true, "LF"], 11 | "max-classes-per-file": false, 12 | "max-line-length": [true, 100], 13 | "member-access": [true, "no-public"], 14 | "member-ordering": [ 15 | true, 16 | { 17 | "order": [ 18 | "public-static-field", 19 | "public-static-method", 20 | "protected-static-field", 21 | "protected-static-method", 22 | "private-static-field", 23 | "private-static-method", 24 | "public-instance-field", 25 | "protected-instance-field", 26 | "private-instance-field", 27 | "public-constructor", 28 | "protected-constructor", 29 | "private-constructor", 30 | "public-instance-method", 31 | "protected-instance-method", 32 | "private-instance-method" 33 | ] 34 | } 35 | ], 36 | "no-unused-expression": true, 37 | "no-unused-variable": true, 38 | "no-duplicate-variable": true, 39 | "no-implicit-dependencies": false, 40 | "no-namespace": false, 41 | "no-submodule-imports": false, 42 | "no-trailing-whitespace": true, 43 | "no-var-keyword": true, 44 | "object-literal-sort-keys": false, 45 | "ordered-imports": true, 46 | "semicolon": [true, "always", "ignore-bound-class-methods"], 47 | "variable-name": false, 48 | "no-this-assignment": [true, { "allow-destructuring": true }] 49 | }, 50 | "extends": ["tslint:latest", "tslint-config-prettier"], 51 | "linterOptions": { 52 | "exclude": ["@here/**/**/*.d.ts", "**/*.json"] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/text-editor-frame/components-smart/Notifications.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 | 7 | import * as React from "react"; 8 | import TextButton from "../../components/TextButton"; 9 | import { Notification } from "../../types"; 10 | import textEditor from "../TextEditor"; 11 | 12 | interface State { 13 | notifications: Notification[]; 14 | } 15 | 16 | export default class extends React.Component { 17 | private onNotificationUpdate: (notifications: Notification[]) => void; 18 | constructor(props: {}) { 19 | super(props); 20 | this.state = { 21 | notifications: [], 22 | }; 23 | 24 | this.onNotificationUpdate = (notifications) => { 25 | this.setState({ notifications }); 26 | }; 27 | } 28 | 29 | componentDidMount() { 30 | textEditor.on("updateNotifications", this.onNotificationUpdate); 31 | } 32 | 33 | componentWillUnmount() { 34 | textEditor.removeListener("updateNotifications", this.onNotificationUpdate); 35 | } 36 | 37 | render() { 38 | const notifications = this.state.notifications; 39 | 40 | return ( 41 |
42 |
    43 | {notifications.map((message, i) => { 44 | return ( 45 |
  • 6 ? "error" : ""} key={i}> 46 | { 48 | textEditor.setCursor( 49 | message.startLineNumber, 50 | message.startColumn 51 | ); 52 | }} 53 | > 54 | {/*tslint:disable-next-line: max-line-length*/} 55 | {`${message.startLineNumber}:${message.startColumn} ${message.message}`} 56 | 57 |
  • 58 | ); 59 | })} 60 |
61 |
62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@here/harp-map-editor", 3 | "version": "0.1.2", 4 | "description": "A simple online editor for harp.gl themes.", 5 | "author": { 6 | "name": "HERE Europe B.V.", 7 | "url": "https://here.com" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/heremaps/harp-map-editor.git" 12 | }, 13 | "license": "Apache-2.0", 14 | "scripts": { 15 | "start": "webpack-dev-server -d", 16 | "build": "NODE_ENV=production npx --node-arg '--max-old-space-size=2048' webpack -p", 17 | "test": "echo 'Harp map editor'", 18 | "typedoc": "typedoc --disableOutputCheck --options typedoc.json", 19 | "tslint": "tslint --project tsconfig.json", 20 | "tslint:fix": "tslint --fix --project tsconfig.json", 21 | "prettier": "prettier -l '**/*.ts' '**/*.tsx' '**/*.json'", 22 | "prettier:fix": "prettier --write '**/*.ts' '**/*.tsx' '**/*.json'" 23 | }, 24 | "publishConfig": { 25 | "access": "public" 26 | }, 27 | "engines": { 28 | "node": ">=9.11.1", 29 | "npm": ">=5.8.0", 30 | "yarn": ">=1.11.1" 31 | }, 32 | "devDependencies": { 33 | "@here/harp-font-resources": "~0.2.4", 34 | "@here/harp-map-controls": "~0.19.0", 35 | "@here/harp-map-theme": "~0.19.0", 36 | "@here/harp-mapview": "~0.19.0", 37 | "@here/harp-vectortile-datasource": "~0.19.1", 38 | "@types/react": "^16.8.18", 39 | "@types/react-dom": "^16.8.4", 40 | "@types/react-json-tree": "^0.6.11", 41 | "@types/throttle-debounce": "^2.1.0", 42 | "copy-webpack-plugin": "^5.1.1", 43 | "css-loader": "^3.2.0", 44 | "file-loader": "^6.0.0", 45 | "html-webpack-plugin": "^4.3.0", 46 | "jszip": "^3.2.1", 47 | "monaco-editor": "^0.20.0", 48 | "monaco-editor-webpack-plugin": "^1.7.0", 49 | "node-sass": "^4.12.0", 50 | "prettier": "^2.0.5", 51 | "react": "^16.8.6", 52 | "react-dom": "^16.8.6", 53 | "react-icons": "^3.7.0", 54 | "react-json-tree": "^0.11.2", 55 | "sass-loader": "^8.0.0", 56 | "style-loader": "^1.0.0", 57 | "three": "^0.119.0", 58 | "throttle-debounce": "^2.1.0", 59 | "ts-loader": "^7.0.5", 60 | "tslint": "^6.1.2", 61 | "tslint-config-prettier": "^1.18.0", 62 | "typedoc": "^0.17.7", 63 | "typescript": "^3.6.4", 64 | "webpack": "^4.32.2", 65 | "webpack-cli": "^3.3.2", 66 | "webpack-dev-server": "^3.4.1", 67 | "webpack-merge": "^4.2.1" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/map-editor/components-smart/PopupGeometriesList.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017-2019 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 JSONTree from "react-json-tree"; 8 | import { TechniqueData, WhenPropsData } from "../../types"; 9 | import ButtonIcon, { ICONS } from "../components/ButtonIcon"; 10 | import MapHandler from "../map-handler"; 11 | import { geometryList } from "../map-handler/MapGeometryList"; 12 | import Menu from "./Menu"; 13 | 14 | interface Props { 15 | done: () => void; 16 | } 17 | 18 | export default class extends React.Component { 19 | render() { 20 | return ( 21 |
22 | { 27 | if ( 28 | (data as WhenPropsData).$layer && 29 | (data as WhenPropsData).$geometryType 30 | ) { 31 | const styleData = new TechniqueData(); 32 | styleData.when = MapHandler.whenFromKeyVal(data as WhenPropsData); 33 | styleData.layer = (data as WhenPropsData).$layer; 34 | styleData.geometryType = (data as WhenPropsData).$geometryType; 35 | return ( 36 | 37 | {itemType} {itemString}{" "} 38 | { 43 | Menu.openNewTechniquePopup(styleData); 44 | this.props.done(); 45 | }} 46 | /> 47 | 48 | ); 49 | } 50 | return ( 51 | 52 | {itemType} {itemString} 53 | 54 | ); 55 | }} 56 | /> 57 |
58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/map-editor/map-handler/MapHighliter.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 | import { APIFormat, VectorTileDataSource } from "@here/harp-vectortile-datasource"; 8 | import { VectorTileDecoder } from "@here/harp-vectortile-datasource/lib/VectorTileDecoder"; 9 | import { accessToken } from "../config"; 10 | import settings from "../Settings"; 11 | import MapHandler from "./index"; 12 | 13 | class MapHighlighter { 14 | private m_activeWhereParam = ""; 15 | private m_highlightDataSource: VectorTileDataSource; 16 | 17 | private m_style: any = { 18 | when: "0", 19 | renderOrder: Number.MAX_SAFE_INTEGER, 20 | styles: [ 21 | { 22 | when: "$geometryType == 'line'", 23 | technique: "line", 24 | attr: { 25 | transparent: true, 26 | color: "red", 27 | }, 28 | }, 29 | { 30 | when: "$geometryType == 'polygon'", 31 | technique: "fill", 32 | attr: { 33 | transparent: true, 34 | color: "red", 35 | }, 36 | }, 37 | { 38 | when: "$geometryType == 'point'", 39 | technique: "circles", 40 | attr: { 41 | color: "red", 42 | size: 10, 43 | }, 44 | }, 45 | ], 46 | }; 47 | 48 | constructor() { 49 | this.m_highlightDataSource = new VectorTileDataSource({ 50 | name: "decorations", 51 | baseUrl: "https://xyz.api.here.com/tiles/herebase.02", 52 | apiFormat: APIFormat.XYZOMV, 53 | styleSetName: "tilezen", 54 | maxDisplayLevel: 17, 55 | authenticationCode: accessToken, 56 | decoder: new VectorTileDecoder(), 57 | }); 58 | } 59 | 60 | highlight = async (newWhenCondition: string) => { 61 | const editorCurrentStyle = settings.get("editorCurrentStyle"); 62 | 63 | if (editorCurrentStyle === null || MapHandler.mapView === null) { 64 | return; 65 | } 66 | 67 | if (MapHandler.mapView.getDataSourceByName(this.m_highlightDataSource.name) === undefined) { 68 | await MapHandler.mapView.addDataSource(this.m_highlightDataSource); 69 | 70 | this.m_highlightDataSource.styleSetName = editorCurrentStyle; 71 | this.m_highlightDataSource.setStyleSet([this.m_style]); 72 | } 73 | 74 | if (this.m_activeWhereParam !== newWhenCondition) { 75 | this.m_style.when = this.m_activeWhereParam = newWhenCondition || "0"; 76 | this.m_highlightDataSource.setStyleSet([this.m_style]); 77 | 78 | MapHandler.mapView.clearTileCache(this.m_highlightDataSource.name); 79 | MapHandler.mapView.update(); 80 | } 81 | }; 82 | } 83 | 84 | export default new MapHighlighter(); 85 | -------------------------------------------------------------------------------- /src/map-editor/components/ButtonIcon.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 { 8 | FiAlertTriangle, 9 | FiBox, 10 | FiCheck, 11 | FiCheckSquare, 12 | FiCopy, 13 | FiLink, 14 | FiSidebar, 15 | FiSquare, 16 | FiTerminal, 17 | FiX, 18 | } from "react-icons/fi"; 19 | import { 20 | IoIosColorPalette, 21 | IoIosColorWand, 22 | IoIosCopy, 23 | IoIosFolderOpen, 24 | IoIosRedo, 25 | IoIosUndo, 26 | IoMdCode, 27 | IoMdColorFilter, 28 | IoMdDownload, 29 | IoMdEye, 30 | } from "react-icons/io"; 31 | import { Side } from "../../types"; 32 | 33 | export class ICONS { 34 | static readonly eye = IoMdEye; 35 | static readonly [Side.Bottom] = FiSidebar; 36 | static readonly [Side.Left] = FiSidebar; 37 | static readonly [Side.Right] = FiSidebar; 38 | static readonly [Side.Top] = FiSidebar; 39 | static readonly [Side.DeTouch] = FiCopy; 40 | static readonly copy = IoIosCopy; 41 | static readonly download = IoMdDownload; 42 | static readonly open = IoIosFolderOpen; 43 | static readonly format = IoMdCode; 44 | static readonly picker = IoMdColorFilter; 45 | static readonly commands = FiTerminal; 46 | static readonly check = FiCheck; 47 | static readonly checkOn = FiCheckSquare; 48 | static readonly checkOff = FiSquare; 49 | static readonly colorPalette = IoIosColorPalette; 50 | static readonly close = FiX; 51 | static readonly undo = IoIosUndo; 52 | static readonly redo = IoIosRedo; 53 | static readonly geometries = FiBox; 54 | static readonly magicStick = IoIosColorWand; 55 | static readonly alert = FiAlertTriangle; 56 | static readonly link = FiLink; 57 | } 58 | 59 | export type EventCallBack = (event: React.MouseEvent) => void; 60 | 61 | export interface ButtonIconProps { 62 | icon: React.ComponentFactory; 63 | active?: boolean; 64 | onClick?: EventCallBack; 65 | disabled?: boolean; 66 | label?: string; 67 | title?: string; 68 | className?: string; 69 | } 70 | 71 | export default class ButtonIcon extends React.Component { 72 | render() { 73 | const { onClick, title, disabled, active, label } = this.props; 74 | const Icon = this.props.icon; 75 | let className = "button-icon no-select"; 76 | 77 | if (!Icon) { 78 | throw new Error(); 79 | } 80 | 81 | if (active) { 82 | className += " active"; 83 | } else if (disabled) { 84 | className += " disabled"; 85 | } 86 | 87 | if (this.props.className) { 88 | className += ` ${this.props.className}`; 89 | } 90 | 91 | return ( 92 |
93 | 94 | {label !== undefined ? {label} : null} 95 |
96 | ); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const MonacoWebpackPlugin = require("monaco-editor-webpack-plugin"); 3 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 4 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 5 | const merge = require("webpack-merge"); 6 | 7 | const isProduction = process.env.NODE_ENV === "production"; 8 | const bundleSuffix = isProduction ? ".min" : ""; 9 | 10 | /** @type{import("webpack").Configuration} */ 11 | const commonConfig = { 12 | output: { 13 | filename: `[name].bundle${bundleSuffix}.js` 14 | }, 15 | resolve: { 16 | extensions: [".webpack.js", ".web.ts", ".ts", ".tsx", ".web.js", ".js"] 17 | }, 18 | devtool: isProduction ? "source-map" : "inline-source-map", 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.scss$/, 23 | use: [ 24 | "style-loader", // creates style nodes from JS strings 25 | "css-loader", // translates CSS into CommonJS 26 | "sass-loader" // compiles Sass to CSS, using Node Sass by default 27 | ] 28 | }, 29 | { 30 | test: /\.tsx?$/, 31 | loader: "ts-loader", 32 | exclude: /node_modules/, 33 | options: { 34 | onlyCompileBundledFiles: true 35 | } 36 | }, 37 | { 38 | test: /\.css$/, 39 | loader: ["style-loader", "css-loader"] 40 | }, 41 | { 42 | test: /\.ttf$/, 43 | use: ['file-loader'] 44 | } 45 | ] 46 | }, 47 | plugins: [ 48 | new CopyWebpackPlugin([ 49 | { 50 | from: "node_modules/@here/harp-map-theme/resources", 51 | to: ".", 52 | toType: "dir" 53 | }, 54 | require.resolve("three/build/three.min.js") 55 | ]) 56 | ], 57 | devServer: { 58 | contentBase: path.join(__dirname, "dist") 59 | }, 60 | mode: process.env.NODE_ENV || "development" 61 | }; 62 | 63 | const mapEditorConfig = merge(commonConfig, { 64 | entry: "./src/map-editor/index.tsx", 65 | plugins: [ 66 | new HtmlWebpackPlugin({ 67 | template: "./src/map-editor/index.html" 68 | }) 69 | ] 70 | }); 71 | 72 | const textEditorFrameConfig = merge(commonConfig, { 73 | entry: { 74 | textEditor: "./src/text-editor-frame/index.tsx" 75 | }, 76 | plugins: [ 77 | new MonacoWebpackPlugin(), 78 | new HtmlWebpackPlugin({ 79 | template: "./src/text-editor-frame/textEditor.html", 80 | filename: "textEditor.html" 81 | }) 82 | ] 83 | }); 84 | 85 | /** @type{import("webpack").Configuration} */ 86 | const decoderConfig = { 87 | target: "webworker", 88 | entry: { 89 | decoder: "./src/decoder.ts" 90 | }, 91 | output: { 92 | filename: `[name].bundle${bundleSuffix}.js` 93 | }, 94 | devtool: isProduction ? "source-map" : "inline-source-map", 95 | resolve: { 96 | extensions: [".ts", ".js"] 97 | }, 98 | module: { 99 | rules: [ 100 | { 101 | test: /\.tsx?$/, 102 | loader: "ts-loader", 103 | exclude: /node_modules/, 104 | options: { 105 | onlyCompileBundledFiles: true 106 | } 107 | } 108 | ] 109 | }, 110 | mode: process.env.NODE_ENV || "development" 111 | }; 112 | 113 | module.exports = [mapEditorConfig, textEditorFrameConfig, decoderConfig]; 114 | -------------------------------------------------------------------------------- /src/map-editor/Component.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 * as React from "react"; 7 | import settings, { AvailableData, AvailableSetting } from "./Settings"; 8 | 9 | /** 10 | * Settings state interface the must be implemented for extended [[React.components]]. 11 | */ 12 | export interface SettingsState { 13 | settings: { [Key in keyof AvailableSetting]?: AvailableSetting[Key] }; 14 | store: { [Key in keyof AvailableData]?: AvailableData[Key] }; 15 | } 16 | 17 | /** 18 | * Extends default [[Rect.Component]] for make easy to use [[settings]] data store. 19 | */ 20 | export default class Component< 21 | P = {}, 22 | S extends SettingsState = { settings: {}; store: {} }, 23 | SS = any 24 | > extends React.Component { 25 | private m_settingsListeners: { [s: string]: (val: any) => void } = {}; 26 | private m_eventsListeners: { [s: string]: (val: any) => void } = {}; 27 | private m_storeListeners: { [s: string]: (val: any) => void } = {}; 28 | 29 | /** 30 | * Receives a list settings variables to monitor. If some of specified variables will change the 31 | * React component will update. 32 | */ 33 | connectSettings(list: A[]) { 34 | list.forEach((key) => { 35 | const listener = (val: B) => { 36 | this.setState((state) => { 37 | state.settings[key] = val; 38 | return state; 39 | }); 40 | }; 41 | settings.on(`setting:${key}`, listener); 42 | this.m_settingsListeners[key] = listener; 43 | }); 44 | 45 | this.setState({ settings: settings.read(list) }); 46 | } 47 | 48 | /** 49 | * Receives a list store variables to monitor. If some of specified variables will change the 50 | * React component will update. 51 | */ 52 | connectStore(list: (keyof AvailableData)[]) { 53 | list.forEach((key) => { 54 | const listener = (val: any) => { 55 | this.setState((state) => { 56 | state.store[key] = val; 57 | return state; 58 | }); 59 | }; 60 | settings.on(`store:${key}`, listener); 61 | this.m_storeListeners[key] = listener; 62 | }); 63 | 64 | this.setState({ store: settings.readStore(list) }); 65 | } 66 | 67 | /** 68 | * Receives a list of events with callbacks to monitor. 69 | */ 70 | connectEvents(events: { [s: string]: (val: any) => void }) { 71 | Object.entries(events).forEach(([key, listener]) => { 72 | settings.on(key, listener); 73 | }); 74 | this.m_eventsListeners = events; 75 | } 76 | 77 | componentWillUnmount() { 78 | // When the component is unmounting it will remove all of the observers to prevent memory 79 | // leaks. 80 | this.disconnectEvents(); 81 | this.disconnectSettings(); 82 | this.disconnectStore(); 83 | } 84 | 85 | private disconnectSettings() { 86 | Object.entries(this.m_settingsListeners).forEach(([key, listener]) => { 87 | settings.removeListener(`setting:${key}`, listener); 88 | }); 89 | } 90 | 91 | private disconnectStore() { 92 | Object.entries(this.m_storeListeners).forEach(([key, listener]) => { 93 | settings.removeListener(`store:${key}`, listener); 94 | }); 95 | } 96 | 97 | private disconnectEvents() { 98 | Object.entries(this.m_eventsListeners).forEach(([key, listener]) => { 99 | settings.removeListener(key, listener); 100 | }); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HERE is stopping its engagement on Harp.gl starting 03/01/2022 in favour of a fully productised and production-grade integration of the harp.gl rendering engine into HERE Maps API for JavaScript (https://developer.here.com/develop/javascript-api) All 3D features and many more will be offered via HERE Maps API for JavaScript moving forward. 2 | 3 | # @here/harp-map-editor [![Build Status](https://travis-ci.com/heremaps/harp-map-editor.svg?branch=master)](https://travis-ci.com/heremaps/harp-map-editor) 4 | 5 | ## Overview 6 | 7 | ### A simple online editor for harp.gl themes. 8 | 9 | Allows you to create and edit existing themes. 10 | 11 | The following features are currently available: 12 | - export and import of themes 13 | - live preview 14 | - style change 15 | - restore page state after page reload 16 | - default themes 17 | - code formatting 18 | - theme source code validation 19 | - code autocompletion 20 | - two window mode 21 | 22 | When you run the editor, you should get something similar to the image shown below: 23 | 24 | ![Sample editor](editor.png) 25 | 26 | ## Development 27 | 28 | ### Prerequisites 29 | 30 | - **Node.js** - Please see [nodejs.org](https://nodejs.org/) for installation instructions. 31 | - **Yarn** - Optional. Please see [yarnpkg.com](https://yarnpkg.com/en/) for installation instructions. 32 | 33 | ### Download dependencies 34 | 35 | Run: 36 | 37 | ```sh 38 | npm install 39 | ``` 40 | or 41 | 42 | ```sh 43 | yarn install 44 | ``` 45 | 46 | to download and install all required packages. 47 | 48 | ### Launch development server for harp.gl theme editor 49 | 50 | Run: 51 | 52 | ```sh 53 | yarn start 54 | ``` 55 | 56 | To launch `webpack-dev-server`. Open `http://localhost:8080/` in your favorite browser. 57 | 58 | To build the editor run: 59 | 60 | ```sh 61 | yarn build 62 | ``` 63 | The build result will be in `dist` folder. 64 | 65 | ### Update gh-pages automatically 66 | 67 | In order to update `gh-pages`, you will need to first disable the branch protection, otherwise 68 | TravisCI won't be able to push to that branch. 69 | 70 | Increment the package version and then tag your commit, either locally or if the package version has 71 | been updated already, you can make a release in the GitHub UI and it will create the tag. Note, the 72 | tag must be of the form: `vX.X.X`, because this is how travis knows to publish, see `.travis.yml` 73 | 74 | This will automatically start the job to publish, go to TravisCI and check the status and make sure 75 | you see something like: `Switched to a new branch 'gh-pages'` 76 | 77 | If you have problems, try fixing it manually below. 78 | 79 | ### Update gh-pages manually 80 | 81 | #### Fixing locally 82 | 83 | If you have any trouble with the updating of gh-pages, for example the publish works but the deploy 84 | fails, then restarting the job won't work, because npm will complain that the given package already 85 | exists. Deploying to `gh-pages` will then not be executed. To resolve this, go to the root 86 | directory locally, and run (assuming you have a fresh checkout): 87 | - `git checkout master` 88 | - `yarn && yarn build` 89 | - `mv dist/ ..` 90 | - `git checkout gh-pages` 91 | - `cp -r ../dist/* .` 92 | - `git add *` (you may also need to `git rm` some files if `git status` complains) 93 | - `git push origin gh-pages` 94 | - Go to https://github.com/heremaps/harp-map-editor/settings/branches and select `gh-pages`, and re 95 | enable branch protection. 96 | 97 | #### Fixing via travis-ci 98 | 99 | It is possible to re-run a job with custom yaml, simply remove the `npm publish` step from the 100 | `.travis.yml` and re-run the job. 101 | 102 | Check that the changes are visible: https://heremaps.github.io/harp-map-editor/ 103 | 104 | ## License 105 | 106 | Copyright (C) 2017-2020 HERE Europe B.V. 107 | 108 | See the [LICENSE](./LICENSE) file in the root of this project for license details. 109 | -------------------------------------------------------------------------------- /src/map-editor/map-handler/MapGeometryList.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 { MapEnv } from "@here/harp-datasource-protocol/index-decoder"; 7 | import { GeoCoordinates, mercatorProjection, webMercatorTilingScheme } from "@here/harp-geoutils"; 8 | import { MapView } from "@here/harp-mapview"; 9 | import { DataProvider } from "@here/harp-mapview-decoder"; 10 | import { VectorTileDataSource } from "@here/harp-vectortile-datasource"; 11 | import { OmvDataAdapter } from "@here/harp-vectortile-datasource/lib/adapters/omv/OmvDataAdapter"; 12 | 13 | import { DecodeInfo } from "@here/harp-vectortile-datasource/lib/DecodeInfo"; 14 | import { 15 | IGeometryProcessor, 16 | ILineGeometry, 17 | IPolygonGeometry, 18 | } from "@here/harp-vectortile-datasource/lib/IGeometryProcessor"; 19 | import { Vector2 } from "three"; 20 | 21 | // 22 | 23 | let dataProvider: DataProvider; 24 | export let geometryList: any = {}; 25 | 26 | class Decoder implements IGeometryProcessor { 27 | processLineFeature( 28 | layerName: string, 29 | layerExtents: number, 30 | geometry: ILineGeometry[], 31 | env: MapEnv, 32 | storageLevel: number 33 | ) { 34 | this.dump("line", layerName, env); 35 | } 36 | 37 | processPointFeature( 38 | layerName: string, 39 | layerExtents: number, 40 | geometry: Vector2[], 41 | env: MapEnv, 42 | storageLevel: number 43 | ) { 44 | this.dump("point", layerName, env); 45 | } 46 | 47 | processPolygonFeature( 48 | layerName: string, 49 | layerExtents: number, 50 | geometry: IPolygonGeometry[], 51 | env: MapEnv, 52 | storageLevel: number 53 | ) { 54 | this.dump("polygon", layerName, env); 55 | } 56 | 57 | private dump(type: string, layer: string, env: MapEnv) { 58 | const level = (env.entries.$level as string) + " level"; 59 | const kind = env.entries.kind as string; 60 | geometryList[layer] = geometryList[layer] || {}; 61 | geometryList[layer][type] = geometryList[layer][type] || {}; 62 | geometryList[layer][type][level] = geometryList[layer][type][level] || {}; 63 | geometryList[layer][type][level][kind] = geometryList[layer][type][level][kind] || []; 64 | geometryList[layer][type][level][kind].push(env.entries); 65 | } 66 | } 67 | 68 | /** 69 | * 70 | * @param geoPoint The geo coordinates of a point of the tile. 71 | * @param level The storage level. 72 | * @param projection The target projection used by the map view. 73 | * @param storageLevelOffset The storage level offset. 74 | */ 75 | 76 | async function dumpTile( 77 | geoPoint: GeoCoordinates, 78 | level: number, 79 | projection = mercatorProjection, 80 | storageLevelOffset = 0 81 | ) { 82 | const tileKey = webMercatorTilingScheme.getTileKey(geoPoint, level); 83 | 84 | if (!tileKey) { 85 | throw new Error("failed to get tile"); 86 | } 87 | 88 | geometryList = {}; 89 | const buffer = (await dataProvider.getTile(tileKey)) as ArrayBuffer; 90 | const decoder = new Decoder(); 91 | const adapter = new OmvDataAdapter(decoder); 92 | 93 | const decodeInfo = new DecodeInfo("dump", projection, tileKey, storageLevelOffset); 94 | adapter.process(buffer, decodeInfo); 95 | } 96 | 97 | export const getGeometryData = (mapView: MapView, dataSource: VectorTileDataSource): void => { 98 | const geoPoint = new GeoCoordinates( 99 | mapView.geoCenter.latitude % 180, 100 | mapView.geoCenter.longitude % 180 101 | ); 102 | dataProvider = dataProvider || dataSource.dataProvider(); 103 | 104 | dumpTile(geoPoint, Math.min(mapView.storageLevel, 15)).catch((err) => { 105 | // tslint:disable-next-line 106 | console.log(err); 107 | }); 108 | }; 109 | -------------------------------------------------------------------------------- /src/map-editor/components-smart/Editor.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 SplitView from "../../components/SplitView"; 8 | import { Side } from "../../types"; 9 | import Component, { SettingsState } from "../Component"; 10 | import mapHandler from "../map-handler"; 11 | import settings from "../Settings"; 12 | import TextEditor from "../TextEditor"; 13 | import MapElem from "./MapElem"; 14 | 15 | export default class Editor extends Component { 16 | private m_elemEditorTab: HTMLDivElement | null = null; 17 | 18 | constructor(props: object) { 19 | super(props); 20 | 21 | this.state = { 22 | settings: {}, 23 | store: {}, 24 | }; 25 | 26 | this.connectEvents({ 27 | // toggle visibility of the editor UI. 28 | "editor:toggle": () => { 29 | settings.set("editorTabVisible", !settings.get("editorTabVisible")); 30 | }, 31 | // set the position of the text editor. 32 | "editor:setSide": (side) => { 33 | settings.set("editorTabSide", side); 34 | if (side === Side.DeTouch) { 35 | TextEditor.createWindow(); 36 | } else { 37 | TextEditor.createIframe(); 38 | } 39 | }, 40 | }); 41 | } 42 | 43 | componentWillMount() { 44 | this.connectSettings(["editorTabVisible", "editorTabSize", "editorTabSide"]); 45 | 46 | if (this.state.settings.editorTabSide === Side.DeTouch) { 47 | settings.set("editorTabSide", Side.Left); 48 | } 49 | } 50 | 51 | componentDidMount() { 52 | this.appendEditor(); 53 | } 54 | 55 | componentDidUpdate() { 56 | this.appendEditor(); 57 | } 58 | 59 | render() { 60 | const { editorTabSide, editorTabSize, editorTabVisible } = this.state.settings; 61 | const textEditorVisible = editorTabVisible && editorTabSide !== Side.DeTouch; 62 | 63 | let content = ; 64 | 65 | if (textEditorVisible) { 66 | let tmpComponent; 67 | let layout: "vertical" | "horizontal" = "horizontal"; 68 | let section_a = ( 69 |
(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 |
70 | {popups.map((popup, i) => { 71 | const options = popup.options || {}; 72 | const exitButton = 73 | options.exitGuard === undefined || options.exitGuard === "closeButton"; 74 | 75 | return ( 76 |
{ 81 | if (options.exitGuard === undefined) { 82 | this.closePopup(popup); 83 | } 84 | }} 85 | > 86 |
event.stopPropagation()} 89 | > 90 |
91 | {popup.name} 92 | {exitButton ? ( 93 | this.closePopup(popup)} 97 | /> 98 | ) : null} 99 |
100 |
{popup.component}
101 |
102 |
103 | ); 104 | })} 105 |
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 | 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.