├── .gitignore ├── .npmignore ├── .ort.yml ├── .prettierignore ├── .prettierrc.json ├── .travis.yml ├── LICENSE ├── README.md ├── editor.png ├── package.json ├── scripts └── publish-packages.sh ├── src ├── components │ ├── SplitView.tsx │ └── TextButton.tsx ├── decoder.ts ├── map-editor │ ├── Component.ts │ ├── Settings.ts │ ├── TextEditor.tsx │ ├── components-smart │ │ ├── App.tsx │ │ ├── Editor.tsx │ │ ├── Info.tsx │ │ ├── MapElem.tsx │ │ ├── Menu.tsx │ │ ├── PopupCreateTechnique.tsx │ │ ├── PopupGeometriesList.tsx │ │ ├── PopupSelectLink.tsx │ │ ├── PopupSelectTheme.tsx │ │ └── PopupsContainer.tsx │ ├── components │ │ ├── ButtonIcon.tsx │ │ ├── SelectString.tsx │ │ └── Tabs.tsx │ ├── datasourceSchemaModified.json │ ├── index.html │ ├── index.tsx │ └── map-handler │ │ ├── MapGeometryList.ts │ │ ├── MapHighliter.ts │ │ ├── MapViewState.ts │ │ └── index.ts ├── style.scss ├── text-editor-frame │ ├── TextEditor.ts │ ├── components-smart │ │ ├── App.tsx │ │ ├── Notifications.tsx │ │ └── TextEditorElem.tsx │ ├── harp-theme.vscode.schema.json │ ├── index.tsx │ └── textEditor.html └── types.ts ├── tsconfig.json ├── tslint.json ├── typedoc.json ├── webpack.config.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules/ 2 | dist/ 3 | yarn-error.log -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | src/text-editor-frame/harp-theme.vscode.schema.json 3 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 4 4 | } 5 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heremaps/harp-map-editor/a0cc28097a8c3ca2534ce8185d2a1780295bd666/editor.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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-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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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.