├── .browserslistrc ├── .dockerignore ├── .editorconfig ├── .gitignore ├── 3rdpartylicenses.txt ├── CHANGELOG.md ├── Dockerfile ├── Dockerfile-vt-map-editor-prod ├── Dockerfile-vt-map-view-prod ├── LICENSE.txt ├── README.md ├── angular.json ├── docker ├── nginx-vt-map-editor.conf └── nginx-vt-map-view.conf ├── docs ├── configuration.adoc ├── docker-setup.adoc ├── images │ ├── group_gui_layer_no_metadata.jpg │ ├── group_gui_layer_schema.jpg │ ├── group_gui_layer_slider.jpg │ ├── group_gui_layer_slider_no_detail.jpg │ ├── group_gui_layer_tools.jpg │ ├── kubernetes_cluster.png │ └── vt_map_editor_app.jpg ├── kubernetes.adoc ├── layer_groups.adoc ├── url_parameters.adoc └── vt-map-editor.adoc ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.e2e.json ├── k8s ├── ingress-service.yaml ├── vt-map-editor.yaml ├── vt-map-service.yaml ├── vt-map-view.yaml └── vt-styles.yaml ├── package-lock.json ├── package.json ├── projects └── vt-map-view │ ├── .browserslistrc │ ├── e2e │ ├── protractor.conf.js │ ├── src │ │ ├── app.e2e-spec.ts │ │ └── app.po.ts │ └── tsconfig.json │ ├── karma.conf.js │ ├── src │ ├── app │ │ ├── app-config.service.ts │ │ ├── app.component.html │ │ ├── app.component.scss │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── maplibre-gl │ │ │ ├── maplibre-gl.component.scss │ │ │ ├── maplibre-gl.component.spec.ts │ │ │ └── maplibre-gl.component.ts │ │ └── shared │ │ │ └── settings.model.ts │ ├── assets │ │ ├── .gitkeep │ │ └── config │ │ │ └── config.json │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ ├── polyfills.ts │ ├── styles.scss │ └── test.ts │ ├── tsconfig.app.json │ ├── tsconfig.spec.json │ └── tslint.json ├── src ├── app │ ├── app-config.service.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── feedback │ │ ├── feedback.component.html │ │ ├── feedback.component.scss │ │ ├── feedback.component.spec.ts │ │ └── feedback.component.ts │ ├── header │ │ ├── header.component.html │ │ ├── header.component.scss │ │ ├── header.component.spec.ts │ │ ├── header.component.ts │ │ ├── header.service.spec.ts │ │ └── header.service.ts │ ├── info │ │ ├── info.component.html │ │ ├── info.component.scss │ │ ├── info.component.spec.ts │ │ └── info.component.ts │ ├── map │ │ ├── map-function.service.spec.ts │ │ ├── map-function.service.ts │ │ ├── map-saving.service.spec.ts │ │ ├── map-saving.service.ts │ │ ├── map-styling.service.spec.ts │ │ ├── map-styling.service.ts │ │ ├── map.component.html │ │ ├── map.component.scss │ │ ├── map.component.spec.ts │ │ ├── map.component.ts │ │ ├── maplibre-gl │ │ │ ├── maplibre-gl.component.scss │ │ │ ├── maplibre-gl.component.spec.ts │ │ │ ├── maplibre-gl.component.ts │ │ │ ├── maplibre-gl.pitch.control.ts │ │ │ ├── maplibre-gl.search.control.ts │ │ │ └── maplibre-gl.show-zoom.control.ts │ │ └── tools │ │ │ ├── map-tool.ts │ │ │ ├── tool-basemap │ │ │ ├── tool-basemap.component.html │ │ │ ├── tool-basemap.component.scss │ │ │ ├── tool-basemap.component.spec.ts │ │ │ └── tool-basemap.component.ts │ │ │ ├── tool-edit │ │ │ ├── group-configuration │ │ │ │ ├── group-configuration.component.html │ │ │ │ ├── group-configuration.component.scss │ │ │ │ ├── group-configuration.component.spec.ts │ │ │ │ └── group-configuration.component.ts │ │ │ ├── gui-layer │ │ │ │ ├── gui-layer-configuration.component.html │ │ │ │ ├── gui-layer-configuration.component.scss │ │ │ │ ├── gui-layer-configuration.component.spec.ts │ │ │ │ ├── gui-layer-configuration.component.ts │ │ │ │ ├── gui-layer-element.component.html │ │ │ │ ├── gui-layer-element.component.scss │ │ │ │ ├── gui-layer-element.component.spec.ts │ │ │ │ └── gui-layer-element.component.ts │ │ │ ├── layer │ │ │ │ ├── layer-configuration.component.html │ │ │ │ ├── layer-configuration.component.scss │ │ │ │ ├── layer-configuration.component.spec.ts │ │ │ │ ├── layer-configuration.component.ts │ │ │ │ ├── layer-element.component.html │ │ │ │ ├── layer-element.component.scss │ │ │ │ └── layer-element.component.ts │ │ │ ├── tool-edit.component.html │ │ │ ├── tool-edit.component.scss │ │ │ ├── tool-edit.component.spec.ts │ │ │ └── tool-edit.component.ts │ │ │ ├── tool-functions │ │ │ ├── tool-functions.component.html │ │ │ ├── tool-functions.component.scss │ │ │ ├── tool-functions.component.spec.ts │ │ │ └── tool-functions.component.ts │ │ │ ├── tool-overlay │ │ │ ├── tool-overlay.component.html │ │ │ ├── tool-overlay.component.scss │ │ │ ├── tool-overlay.component.spec.ts │ │ │ └── tool-overlay.component.ts │ │ │ ├── tool-print-editor │ │ │ ├── tool-print-editor.component.html │ │ │ ├── tool-print-editor.component.scss │ │ │ ├── tool-print-editor.component.spec.ts │ │ │ └── tool-print-editor.component.ts │ │ │ └── tool-share │ │ │ ├── tool-share.component.html │ │ │ ├── tool-share.component.scss │ │ │ ├── tool-share.component.spec.ts │ │ │ └── tool-share.component.ts │ ├── material-design │ │ ├── material-design.module.ts │ │ └── vt-map-editor-material-theme.scss │ ├── menu │ │ ├── menu-item.ts │ │ ├── menu.component.html │ │ ├── menu.component.scss │ │ ├── menu.component.spec.ts │ │ └── menu.component.ts │ └── shared │ │ ├── basemap.ts │ │ ├── map-function.ts │ │ ├── map-functions.ts │ │ ├── mapview.ts │ │ └── settings.model.ts ├── assets │ ├── config │ │ └── config.json │ ├── images │ │ ├── github.png │ │ ├── logo.png │ │ └── thumbnails │ │ │ ├── basemap_classic.png │ │ │ ├── basemap_color.png │ │ │ ├── basemap_grayscale.png │ │ │ ├── basemap_light.png │ │ │ ├── basemap_load.png │ │ │ ├── basemap_night.png │ │ │ └── basemap_random.png │ └── templates │ │ ├── legals.html │ │ └── privacy.html ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── karma.conf.js ├── main.ts ├── polyfills.ts ├── styles.scss ├── test.ts ├── testing │ ├── DOMHelper.ts │ └── activated-route-stub.ts ├── tsconfig.app.json ├── tsconfig.spec.json └── tslint.json ├── tsconfig.json └── tslint.json /.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # 5 | # For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed 6 | 7 | > 0.5% 8 | last 2 versions 9 | Firefox ESR 10 | not dead 11 | not IE 9-11 -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | docs 4 | k8s 5 | .git 6 | .gitignore 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events.json 15 | speed-measure-plugin.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | 48 | # Individual config files 49 | /src/assets/config/gui-layers-*.json 50 | 51 | # Documentation (temporary ignored) 52 | /docs/js 53 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v2.0.0 2 | ### Breaking changes 3 | * Map library changed from Mapbox GL JS to [MapLibre GL JS](https://github.com/maplibre/maplibre-gl-js). 4 | 5 | ### New features 6 | * Metadata for groups and gui layers can be defined outside the style files, in additional configuration files. 7 | * Zoom on bounding box of result geometry at address search 8 | 9 | ### Updates 10 | * Framework / library updates: 11 | * Angular version 11 12 | * And more... (see [package.json](package.json)) 13 | 14 | ## v1.3.5 (2021-02-24) 15 | ### New features 16 | * The map automatically zooms in/out when performing an address search 17 | * Initial unit and e2e tests 18 | 19 | ## v1.3.4 (2020-12-22) 20 | ### Bug fixes 21 | * Missing basemap thumbnail for loaded maps. 22 | 23 | ## v1.3.3 (2020-12-15) 24 | ### Bug fixes 25 | * Styles could be posted multiple times when style changes where made 26 | 27 | ## v1.3.2 (2020-11-23) 28 | ### Bug fixes 29 | * Color picker no longer triggers redirection on changes 30 | 31 | ## v1.3.1 (2020-11-18) 32 | ### New features 33 | * Content of tool overlay can be controlled via URL routes, e.g. _https://domain/map/basemap_ opens the overlay with the basemap selection. 34 | 35 | ## v1.3.0 (2020-11-09) 36 | ### New features 37 | * A short description can be added for each basemap in the configuration file. 38 | * New URL parameters to control the initial map view of VT Map Editor and VT Map View: _zoom_, _center_, _bearing_, _pitch_, _bbox_ (see [URL parameter documentation](docs/url_parameters.adoc)) 39 | 40 | ### Bug fixes 41 | * Map functions could not be enabled. 42 | 43 | ## v1.2.1 (2020-10-13) 44 | ### Bug fixes 45 | * Unit tests are basically executable without errors. 46 | 47 | ## v1.2.0 (2020-09-24) 48 | ### New features 49 | * Menu items can be configured via the configuration file. 50 | * Menu items can be external links. 51 | * Change opacity of GUI layers. 52 | * Update Mapbox GL JS to version 1.12.0 53 | 54 | ### Bug fixes 55 | * Alpha channel was removed from color values when the saturation or lightness of the map was changed. 56 | 57 | ## v1.1.0 (2020-07-31) 58 | __VT Map Editor _v1.1_ requieres VT Map Service _v1.1_ as back-end service.__ 59 | ### Changes 60 | * VT Map Editor does not longer communicate directly with external geocoder APIs, but sends the search requests to VT Map Service. 61 | * Default URL paths for the applications in the Docker images changed to _/map-editor_, _/map-view_ and _/map-service_ 62 | 63 | ## v1.0.0 (2020-07-14) 64 | ### Breaking changes 65 | * __VT Map Editor _v1.x_ requieres VT Map Service _v1.x_ as back-end service.__ 66 | * URLs for custom styles and applications are no longer created by the back-end VT Map Service, but by VT Map Editor. 67 | * Parameter _searchService_ was removed from configuration files of VT Map Editor and VT Map View. By now the geolocation search API parameters are requested from VT Map Service. 68 | 69 | ### New features 70 | * New configuration parameter _mapView_ in config.json, for URL of VT Map View. 71 | * Parameter _url_ of configuration parameters _mapService_ and _mapView_ can contain relative URLs. 72 | * Framework / library updates: 73 | * Angular version 10 74 | * Mapbox GL JS version 1.11.1 75 | * And more... (see [package.json](package.json)) 76 | 77 | ## v0.6.1 (2020-04-21) 78 | ### New features 79 | * Update Mapbox GL JS to version 1.9.1 80 | 81 | ## v0.6.0 (2020-04-06) 82 | ### New features 83 | * Load custom basemap at application start by uuid in query parameters 84 | * Enabled location tracking of GeolocationControl in map client 85 | 86 | ### Bug fixes 87 | * Reset group and GUI layer settings on basemap change 88 | * Wrong VT Map Service style URL for loading basemaps 89 | * Load basemap via URL of VT Map View not possible 90 | 91 | ## v0.5.0 (2020-03-09) 92 | Initial release 93 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14.16 2 | 3 | WORKDIR /app 4 | 5 | COPY package.json . 6 | 7 | RUN npm install 8 | RUN npm install -g @angular/cli 9 | 10 | COPY . . 11 | 12 | CMD ng serve --host 0.0.0.0 13 | -------------------------------------------------------------------------------- /Dockerfile-vt-map-editor-prod: -------------------------------------------------------------------------------- 1 | FROM node:14.16 as build 2 | 3 | WORKDIR /app 4 | 5 | COPY package.json . 6 | 7 | RUN npm install 8 | RUN npm install -g @angular/cli 9 | 10 | COPY . . 11 | 12 | RUN ng build vt-map-editor --prod --base-href /map-editor/ 13 | 14 | ####### 15 | 16 | FROM nginx:alpine 17 | 18 | COPY --from=build /app/dist/vt-map-editor /usr/share/nginx/html/map-editor 19 | 20 | COPY docker/nginx-vt-map-editor.conf /etc/nginx/nginx.conf 21 | 22 | EXPOSE 80 23 | 24 | CMD ["nginx", "-g", "daemon off;"] 25 | -------------------------------------------------------------------------------- /Dockerfile-vt-map-view-prod: -------------------------------------------------------------------------------- 1 | FROM node:14.16 as build 2 | 3 | WORKDIR /app 4 | 5 | COPY package.json . 6 | 7 | RUN npm install 8 | RUN npm install -g @angular/cli 9 | 10 | COPY . . 11 | 12 | RUN ng build vt-map-view --prod --base-href /map-view/ 13 | 14 | ####### 15 | 16 | FROM nginx:alpine 17 | 18 | COPY --from=build /app/dist/vt-map-view /usr/share/nginx/html/map-view 19 | 20 | COPY docker/nginx-vt-map-view.conf /etc/nginx/nginx.conf 21 | 22 | EXPOSE 80 23 | 24 | CMD ["nginx", "-g", "daemon off;"] 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VT Map Editor 2 | 3 | THIS SOFTWARE IS NO LONGER ACTIVELY MAINTAINED. 4 | 5 | Web editor for custom vector tile styles and map applications, based on Angular and MapLibre GL JS. 6 | 7 |  8 | 9 | Besides VT Map Editor the Angular workspace contains an additional project "vt-map-view". VT Map View serves as simple map viewer for maps created with VT Map Editor. 10 | 11 | ## Getting Started 12 | 13 | To set up the development environment for VT Map Editor on your local machine you can follow these instructions. You can alternatively create a Docker image as you can see below. 14 | 15 | ### Prerequisites 16 | 17 | To get VT Map Editor up and running locally you first need to install a current Node.js LTS version and Angular CLI 10. 18 | 19 | To be able to share map styles and map applications created with VT Map Editor you also need to set up the back-end service [VT Map Service](https://github.com/Basisvisualisierung/vt-map-service). 20 | 21 | ### Installation 22 | 23 | For configuration options see [configuration file documentation](docs/configuration.adoc). 24 | 25 | Open a command prompt and navigate to the project folder. Then install the dependencies: 26 | 27 | ``` 28 | npm install 29 | ``` 30 | 31 | Run VT Map Editor on port 4200: 32 | 33 | ``` 34 | ng serve 35 | ``` 36 | 37 | Run VT Map View on port 4201: 38 | 39 | ``` 40 | ng serve --project vt-map-view --port 4201 41 | ``` 42 | 43 | Build VT Map Editor and VT Map View: 44 | 45 | ``` 46 | ng build vt-map-editor --prod --base-href /map-editor/ 47 | ng build vt-map-view --prod --base-href /map-view/ 48 | ``` 49 | 50 | ## Docker 51 | 52 | First customize the [configuration file](docs/configuration.adoc). 53 | 54 | Open a command prompt, navigate to the project folder and build a Docker image: 55 | 56 | ``` 57 | docker build -t vt-map-editor . 58 | ``` 59 | 60 | Start a container with VT Map Editor: 61 | 62 | ``` 63 | docker run --rm --name vt-map-editor -v ${PWD}:/app -v /app/node_modules -p 4200:4200 -p 4201:4201 vt-map-editor:latest 64 | ``` 65 | 66 | Start VT Map View in the running container: 67 | 68 | ``` 69 | docker exec -it vt-map-editor ng serve --project vt-map-view --host 0.0.0.0 --port 4201 70 | ``` 71 | 72 | For a complete development environment with all related services using Docker Compose see [Docker setup](https://github.com/Basisvisualisierung/vt-map-editor/blob/master/docs/docker-setup.adoc). 73 | 74 | ## Documentation 75 | You can find more details about the configuration and setup of the application in the [documentation](docs/vt-map-editor.adoc). 76 | 77 | ## License 78 | Licensed under the European Union Public License (EUPL). For more information see [LICENSE.txt](LICENSE.txt). 79 | 80 | Copyright 2020-2021 Landesamt für Geoinformation und Landesvermessung Niedersachsen 81 | -------------------------------------------------------------------------------- /docker/nginx-vt-map-editor.conf: -------------------------------------------------------------------------------- 1 | worker_processes 1; 2 | 3 | events { 4 | worker_connections 1024; 5 | } 6 | 7 | http { 8 | server { 9 | listen 80; 10 | server_name localhost; 11 | 12 | root /usr/share/nginx/html; 13 | index index.html index.htm; 14 | include /etc/nginx/mime.types; 15 | 16 | gzip on; 17 | gzip_min_length 1000; 18 | gzip_proxied expired no-cache no-store private auth; 19 | gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript; 20 | 21 | location / { 22 | return 301 /map-editor/; 23 | } 24 | 25 | location /map-editor/ { 26 | try_files $uri $uri/ /map-editor/index.html; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /docker/nginx-vt-map-view.conf: -------------------------------------------------------------------------------- 1 | worker_processes 1; 2 | 3 | events { 4 | worker_connections 1024; 5 | } 6 | 7 | http { 8 | server { 9 | listen 80; 10 | server_name localhost; 11 | 12 | root /usr/share/nginx/html; 13 | index index.html index.htm; 14 | include /etc/nginx/mime.types; 15 | 16 | gzip on; 17 | gzip_min_length 1000; 18 | gzip_proxied expired no-cache no-store private auth; 19 | gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript; 20 | 21 | location / { 22 | return 301 /map-view/; 23 | } 24 | 25 | location /map-view/ { 26 | try_files $uri $uri/ /map-view/index.html; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /docs/configuration.adoc: -------------------------------------------------------------------------------- 1 | = Configuration file 2 | 3 | The configuration of the application is defined by the JSON file `config.json` in the folder `src/assets/config`. 4 | 5 | == Template 6 | Here is a template for the configuration: 7 | 8 | ``` 9 | { 10 | "titles": { 11 | "map": "Basisvisualisierung" 12 | }, 13 | "mapService": { 14 | "url": "/map-service" 15 | }, 16 | "mapView": { 17 | "url": "/map-view" 18 | }, 19 | "map": { 20 | "maxZoom": 18, 21 | "startCenter": [9.361699,52.104253], 22 | "startZoom": 13, 23 | "showZoomLevel": false, 24 | "showScaleBar": true 25 | }, 26 | "mapFunctions": { 27 | "navigation": { 28 | "show": true, 29 | "enabled": true 30 | }, 31 | "info": { 32 | "show": true, 33 | "enabled": true 34 | }, 35 | "search": { 36 | "show": true, 37 | "enabled": true 38 | } 39 | }, 40 | "basemaps": [ 41 | { 42 | "name": "My Map", 43 | "imgUrl": "assets/images/thumbnails/basemap_color.png", 44 | "styling": "assets/basemaps/style.json", 45 | "description": "My colored map" 46 | } 47 | ], 48 | "menuItems": [ 49 | { 50 | "label": "Map", 51 | "icon": "map", 52 | "link": "map", 53 | "externalLink": false 54 | }, 55 | { 56 | "label": "Legals", 57 | "icon": "lock", 58 | "link": "https://domain/path/...", 59 | "externalLink": true 60 | } 61 | "guiLayers": { 62 | "sortByName": true 63 | } 64 | } 65 | ``` 66 | 67 | == Reference 68 | === titles 69 | `map`: string 70 | 71 | Header Title of page _map_. It can contain HTML tags. 72 | 73 | === mapService 74 | `url`: string 75 | 76 | URL of the back end service _VT Map Service_. It can be a relative URL, if VT Map Editor and VT Map Service are running on the same domain. 77 | 78 | === mapView 79 | `url`: string 80 | 81 | URL of application _VT Map View_. It can be a relative URL, if VT Map Editor and VT Map View are running on the same domain. 82 | 83 | === map 84 | `maxZoom`: integer 85 | 86 | Maximum zoom level of the map (\<= 22). 87 | 88 | `startCenter`: [integer, integer] 89 | 90 | Initial center coordinates of the map [latitude, longitude]. 91 | 92 | `startZoom`: integer 93 | 94 | Initial zoom level of the map (\<= 2vt-map-view 95 | Display current zoom level on the bottom of the map. 96 | 97 | `showScaleBar`: true | false 98 | 99 | Display a scale bar on the bottom of the map. 100 | 101 | === mapFunctions 102 | ==== navigation 103 | `show`: true | false 104 | 105 | Show / hide the toggle for the navigation function in the functions user interface. 106 | 107 | `enabled`: true | false 108 | 109 | Enable or disable the toggle by default. 110 | 111 | ==== info 112 | `show`: true | false 113 | 114 | Show / hide the toggle for the info function in the functions user interface. 115 | 116 | `enabled`: true | false 117 | 118 | Enable or disable the toggle by default. 119 | 120 | ==== search 121 | `show`: true | false 122 | 123 | Show / hide the toggle for the search function in the functions user interface. Settings `searchApi` and `searchApiKey` must be defined. 124 | 125 | `enabled`: true | false 126 | 127 | Enable or disable the toggle by default. 128 | 129 | ===== configuration 130 | `color`: string 131 | 132 | Hex color code (e.g. #FF0000) of the route. 133 | 134 | === basemaps 135 | List of available basemaps in the application. Each item has the following attributes: 136 | 137 | `name`: string 138 | 139 | Label of the basemap. 140 | 141 | `imgUrl`: string 142 | 143 | URL to a thumbnail of the basemap styling. It can be a URL or a relative file path. 144 | 145 | `styling`: string 146 | 147 | URL to the JSON styling file of the basemap. It can be an absolute URL, a relative URL (with leading slash) or a relative file path (without leading slash). 148 | 149 | `description`: string 150 | 151 | Short description of the basemap displayed at a mouse over event. + 152 | This attribute ist optional. 153 | 154 | `randomColors`: true | false 155 | 156 | If set to _true_ the current map styling will be rendered with random colors when the button for this basemap is clicked. + 157 | This attribute is optional. The default value is _false_ when the attribute is not specified. 158 | 159 | `metadataFile`: string 160 | 161 | The filename of an additional json file for style metadata created in the folder `src/assets/config`. For more information see link:layer_groups.adoc#configuration-file-for-the-metadata[configuration file]. + 162 | This attribute is optional. When using this attribute all layer metadata from the style file will be ignored by VT Map Editor. 163 | 164 | === menuItems 165 | This array defines the items of the sidebar menu. Each item has the following attributes: 166 | 167 | `label`: string 168 | 169 | Label of the menu item that is displayed in the user interface. 170 | 171 | `icon`: string 172 | 173 | Name of the Material Design icon that is displayed in the user interface. For available icons see https://material.io/resources/icons 174 | 175 | `link`: string 176 | 177 | Either an Angular routing path (externalLink = false) of any URL (externalLink = true) 178 | 179 | `externalLink`: true | false 180 | 181 | If set to _true_ the menu item is a link that opens in a new browser window. 182 | 183 | === guiLayers 184 | `sortByName`: true | false 185 | 186 | true: Sort groups and group layers by their names. + 187 | false: Sort groups and group layers by their appearance in the JSON styling. 188 | 189 | 190 | 191 | -------------------------------------------------------------------------------- /docs/images/group_gui_layer_no_metadata.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Basisvisualisierung/vt-map-editor/20daf75105c50a236f4cd4664c7c28445131bfcc/docs/images/group_gui_layer_no_metadata.jpg -------------------------------------------------------------------------------- /docs/images/group_gui_layer_schema.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Basisvisualisierung/vt-map-editor/20daf75105c50a236f4cd4664c7c28445131bfcc/docs/images/group_gui_layer_schema.jpg -------------------------------------------------------------------------------- /docs/images/group_gui_layer_slider.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Basisvisualisierung/vt-map-editor/20daf75105c50a236f4cd4664c7c28445131bfcc/docs/images/group_gui_layer_slider.jpg -------------------------------------------------------------------------------- /docs/images/group_gui_layer_slider_no_detail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Basisvisualisierung/vt-map-editor/20daf75105c50a236f4cd4664c7c28445131bfcc/docs/images/group_gui_layer_slider_no_detail.jpg -------------------------------------------------------------------------------- /docs/images/group_gui_layer_tools.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Basisvisualisierung/vt-map-editor/20daf75105c50a236f4cd4664c7c28445131bfcc/docs/images/group_gui_layer_tools.jpg -------------------------------------------------------------------------------- /docs/images/kubernetes_cluster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Basisvisualisierung/vt-map-editor/20daf75105c50a236f4cd4664c7c28445131bfcc/docs/images/kubernetes_cluster.png -------------------------------------------------------------------------------- /docs/images/vt_map_editor_app.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Basisvisualisierung/vt-map-editor/20daf75105c50a236f4cd4664c7c28445131bfcc/docs/images/vt_map_editor_app.jpg -------------------------------------------------------------------------------- /docs/kubernetes.adoc: -------------------------------------------------------------------------------- 1 | # Kubernetes deployment for VT Map Editor 2 | 3 | The folder link:../k8s[k8s] in this repository contains configuration files for setting up a Kubernetes cluster with VT Map Editor and related applications and services. 4 | 5 | ## Cluster 6 | 7 | image::images/kubernetes_cluster.png[] 8 | -------------------------------------------------------------------------------- /docs/url_parameters.adoc: -------------------------------------------------------------------------------- 1 | # URL parameters 2 | 3 | The initial map view of the applications VT Map Editor and VT Map View can be controlled by these URL parameters: 4 | 5 | `zoom`: number 6 | 7 | Zoom level between 0 and 22. 8 | 9 | `center`: number,number 10 | 11 | Longitude and latitude for the center of the map, e.g. "9.1,52". 12 | 13 | `pitch`: number 14 | 15 | Map tilt in degrees (0 - 60). 16 | 17 | `bearing`: number 18 | 19 | Rotation of the map in degrees (0 - 360). 20 | 21 | ''' 22 | 23 | Additionally VT Map Editor has the following URL parameter: 24 | 25 | `bbox`: number,number,number,number 26 | 27 | Bounding box of the map (west, south, east, north) e.g. "9,52.1,9.54,53".+ 28 | -------------------------------------------------------------------------------- /docs/vt-map-editor.adoc: -------------------------------------------------------------------------------- 1 | = VT Map Editor documentation 2 | 3 | == Configuration 4 | link:configuration.adoc[Configuration reference] 5 | 6 | link:layer_groups.adoc[Defining layer groups in style files] 7 | 8 | == Setup 9 | link:docker-setup.adoc[Docker setup] 10 | 11 | link:kubernetes.adoc[Kubernetes deployment] 12 | 13 | == Use of the application 14 | link:url_parameters.adoc[URL parameters] 15 | -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './src/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost/map-editor/map', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: require('path').join(__dirname, './tsconfig.e2e.json') 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import {browser, by, element, logging} from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | const sleep = 0; 7 | 8 | beforeEach(() => { 9 | page = new AppPage(); 10 | }); 11 | 12 | it('should display Title', () => { 13 | page.navigateTo(); 14 | expect(page.getTitleText()).toEqual('Basisvisualisierung'); 15 | browser.sleep(sleep); 16 | }); 17 | 18 | it('should be possible to change color of guiLayer', () => { 19 | page.navigateTo(); 20 | page.clickToolButton(1); 21 | page.clickDetailButton(); 22 | page.openGuiLayerGroup(0); 23 | page.changeColor('100', '150', '111', '0.5'); 24 | expect(page.getColor()).toEqual('rgba(100, 150, 111, 0.5)'); 25 | browser.sleep(sleep); 26 | }); 27 | 28 | it('should be possible to change color of layers', () => { 29 | page.navigateTo('/edit/layer'); 30 | page.openLayerElement(0); 31 | page.changeLayerColor('#503616'); 32 | expect(page.getColor()).toEqual('rgba(80, 54, 22, 1)' ); 33 | browser.sleep(sleep); 34 | }); 35 | 36 | it('should be possible to show tooltip on map', () => { 37 | page.navigateTo(); 38 | page.clickMap(200, 750); 39 | expect(page.getTooltipStatus()).toBe(true); 40 | browser.sleep(sleep * 3); 41 | }); 42 | 43 | describe('tests related to functions declaration', () => { 44 | 45 | it('should be possible to remove control elements from the map', () => { 46 | page.navigateTo('/functions'); 47 | page.clickSlider(0); 48 | expect(page.getZoomControlStatus()).toBe(false); 49 | browser.sleep(sleep); 50 | }); 51 | 52 | it('should be possible to remove function to create tooltip from the map', () => { 53 | page.navigateTo('/functions'); 54 | page.clickSlider(1); 55 | page.clickMap(200, 750); 56 | expect(page.getTooltipStatus()).toBe(false); 57 | browser.sleep(sleep * 3); 58 | }); 59 | 60 | it('should be possible to remove search from the map', () => { 61 | page.navigateTo('/functions'); 62 | page.clickSlider(2); 63 | expect(page.getSearchStatus()).toBe(false); 64 | browser.sleep(sleep); 65 | }); 66 | }); 67 | 68 | 69 | describe('tests related to navigation', () => { 70 | 71 | it('should navigate to /basemap if basemap is selected from toolbar', () => { 72 | page.navigateTo(); 73 | page.clickToolButton(0); 74 | expect(browser.getCurrentUrl()).toEqual(`${browser.baseUrl}/basemap`); 75 | }); 76 | 77 | it('should navigate to /edit/group-layer if layers is selected from toolbar', () => { 78 | page.navigateTo(); 79 | page.clickToolButton(1); 80 | expect(browser.getCurrentUrl()).toEqual(`${browser.baseUrl}/edit/group-layer`); 81 | }); 82 | 83 | it('should navigate to /edit/gui-layer if details is selected under group-layer', () => { 84 | page.navigateTo(); 85 | page.clickToolButton(1); 86 | page.clickDetailButton(); 87 | expect(browser.getCurrentUrl()).toEqual(`${browser.baseUrl}/edit/gui-layer`); 88 | }); 89 | 90 | it('should navigate to /functions if settings is selected from toolbar', () => { 91 | page.navigateTo(); 92 | page.clickToolButton(2); 93 | expect(browser.getCurrentUrl()).toEqual(`${browser.baseUrl}/functions`); 94 | }); 95 | 96 | it('should navigate to /share if share is selected from toolbar', () => { 97 | page.navigateTo(); 98 | page.clickToolButton(3); 99 | expect(browser.getCurrentUrl()).toEqual(`${browser.baseUrl}/share`); 100 | }); 101 | }); 102 | 103 | afterEach(async () => { 104 | // Assert that there are no errors emitted from the browser 105 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 106 | expect(logs).not.toContain(jasmine.objectContaining({ 107 | level: logging.Level.SEVERE, 108 | } as logging.Entry)); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | 5 | toolButtons = element.all(by.css('.tool-button')); 6 | detailButtons = element.all(by.css('.btn-detail')); 7 | colorButtons = element.all(by.css('.color-btn')); 8 | canvas = element.all(by.css('.mapboxgl-canvas')); 9 | rgbaTextField = element.all(by.css('.rgba-text')); 10 | rgbaInput = this.rgbaTextField.all(by.css('input')); 11 | okButton = element.all(by.css('.cp-ok-button-class')); 12 | hexTextField = element.all(by.css('.hex-text')); 13 | hexInput = this.hexTextField.all(by.css('input')); 14 | layerDropdown = element.all(by.css('.mat-expansion-panel')); 15 | headlineTitle = element(by.css('.header-title')); 16 | tooltip = element.all(by.css('.mapboxgl-popup-content')); 17 | zoomButton = element.all(by.css('.mapboxgl-ctrl-zoom-in')); 18 | searchButton = element.all(by.css('.search-control')); 19 | 20 | navigateTo(path: string = '') { 21 | return browser.get(`${browser.baseUrl}${path}`) as Promise; 22 | } 23 | 24 | getTitleText() { 25 | return this.headlineTitle.getText(); 26 | } 27 | 28 | getTooltipStatus() { 29 | return this.tooltip.isPresent(); 30 | } 31 | 32 | getZoomControlStatus(){ 33 | return this.zoomButton.isPresent(); 34 | } 35 | 36 | getSearchStatus() { 37 | return this.searchButton.isPresent(); 38 | } 39 | 40 | clickToolButton(index: number = 1) { 41 | this.toolButtons.get(index).click(); 42 | } 43 | 44 | clickDetailButton() { 45 | 46 | this.detailButtons.get(0).click(); 47 | } 48 | 49 | clickSlider(index: number) { 50 | element.all(by.css('.mat-slide-toggle-bar')).get(index).click(); 51 | } 52 | 53 | openGuiLayerGroup(index: number = 0) { 54 | const guiLayerGroup = element(by.id(`mat-expansion-panel-header-${index}`)); 55 | guiLayerGroup.click(); 56 | browser.sleep(500); 57 | } 58 | 59 | clickColorButton(index = 0 ) { 60 | this.colorButtons.get(index).click(); 61 | } 62 | 63 | clickMap(toRight: number, toBottom: number) { 64 | browser.sleep(500); 65 | browser.actions() 66 | .mouseMove(this.canvas.get(0).getWebElement(), { 67 | x: toRight, 68 | y: toBottom, 69 | }) 70 | .perform().then(() => browser.actions() 71 | .click() 72 | .perform()); 73 | } 74 | 75 | changeColor(r: string, g: string, b: string, a: string) { 76 | this.clickColorButton(); 77 | this.rgbaInput.clear(); 78 | this.rgbaInput.get(0).sendKeys(r); 79 | this.rgbaInput.get(1).sendKeys(g); 80 | this.rgbaInput.get(2).sendKeys(b); 81 | this.rgbaInput.get(3).sendKeys(a); 82 | this.okButton.get(0).click(); 83 | } 84 | 85 | changeLayerColor(hexColor: string) { 86 | this.clickColorButton(); 87 | this.hexInput.clear(); 88 | this.hexInput.get(0).sendKeys(hexColor); 89 | this.okButton.get(0).click(); 90 | } 91 | 92 | getColor(index: number = 0) { 93 | browser.sleep(500); 94 | return this.colorButtons.get(index).getCssValue('background-color'); 95 | } 96 | 97 | 98 | openLayerElement(index: number) { 99 | this.layerDropdown.get(index).click(); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /k8s/ingress-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Ingress 3 | metadata: 4 | name: ingress-service 5 | annotations: 6 | kubernetes.io/ingress.class: nginx 7 | nginx.ingress.kubernetes.io/rewrite-target: /$1 8 | nginx.ingress.kubernetes.io/use-regex: "true" 9 | spec: 10 | rules: 11 | - http: 12 | paths: 13 | - path: /(map-editor/?.*) 14 | backend: 15 | serviceName: vt-map-editor-cluster-ip 16 | servicePort: 8080 17 | - path: /map-service/(.*) 18 | backend: 19 | serviceName: vt-map-service-cluster-ip 20 | servicePort: 8081 21 | - path: /(map-view/?.*) 22 | backend: 23 | serviceName: vt-map-view-cluster-ip 24 | servicePort: 8082 25 | - path: /([styles|fonts|sprites]/?.*) 26 | backend: 27 | serviceName: vt-styles-cluster-ip 28 | servicePort: 8083 29 | -------------------------------------------------------------------------------- /k8s/vt-map-editor.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: vt-map-editor-cluster-ip 5 | spec: 6 | type: ClusterIP 7 | selector: 8 | app: vt-map-editor 9 | ports: 10 | - port: 8080 11 | targetPort: 80 12 | --- 13 | apiVersion: apps/v1 14 | kind: Deployment 15 | metadata: 16 | name: vt-map-editor-deployment 17 | spec: 18 | replicas: 1 19 | selector: 20 | matchLabels: 21 | app: vt-map-editor 22 | template: 23 | metadata: 24 | labels: 25 | app: vt-map-editor 26 | tier: frontend 27 | spec: 28 | containers: 29 | - name: vt-map-editor 30 | image: basisvisualisierung/vt-map-editor 31 | ports: 32 | - containerPort: 80 -------------------------------------------------------------------------------- /k8s/vt-map-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolumeClaim 3 | metadata: 4 | name: sqlite-pvc 5 | spec: 6 | accessModes: 7 | - ReadWriteOnce 8 | resources: 9 | requests: 10 | storage: 1Gi 11 | --- 12 | apiVersion: v1 13 | kind: Service 14 | metadata: 15 | name: vt-map-service-cluster-ip 16 | spec: 17 | type: ClusterIP 18 | selector: 19 | service: vt-map-service 20 | ports: 21 | - port: 8081 22 | targetPort: 5000 23 | --- 24 | apiVersion: apps/v1 25 | kind: Deployment 26 | metadata: 27 | name: vt-map-service-deployment 28 | spec: 29 | replicas: 1 30 | selector: 31 | matchLabels: 32 | service: vt-map-service 33 | template: 34 | metadata: 35 | labels: 36 | service: vt-map-service 37 | tier: backend 38 | spec: 39 | volumes: 40 | - name: vt-map-service-sqlite 41 | persistentVolumeClaim: 42 | claimName: sqlite-pvc 43 | containers: 44 | - name: vt-map-service 45 | image: basisvisualisierung/vt-map-service 46 | ports: 47 | - containerPort: 5000 48 | volumeMounts: 49 | - name: vt-map-service-sqlite 50 | mountPath: /service/data 51 | env: 52 | - name: VTMS_SEARCH_API 53 | value: "bkg" 54 | - name: VTMS_SEARCH_API_KEY 55 | valueFrom: 56 | secretKeyRef: 57 | name: bkg-geocoder 58 | key: BKG_GEOCODER_API_KEY -------------------------------------------------------------------------------- /k8s/vt-map-view.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: vt-map-view-cluster-ip 5 | spec: 6 | type: ClusterIP 7 | selector: 8 | app: vt-map-view 9 | ports: 10 | - port: 8082 11 | targetPort: 80 12 | --- 13 | apiVersion: apps/v1 14 | kind: Deployment 15 | metadata: 16 | name: vt-map-view-deployment 17 | spec: 18 | replicas: 1 19 | selector: 20 | matchLabels: 21 | app: vt-map-view 22 | template: 23 | metadata: 24 | labels: 25 | app: vt-map-view 26 | tier: frontend 27 | spec: 28 | containers: 29 | - name: vt-map-view 30 | image: basisvisualisierung/vt-map-view 31 | ports: 32 | - containerPort: 80 -------------------------------------------------------------------------------- /k8s/vt-styles.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: vt-styles-cluster-ip 5 | spec: 6 | type: ClusterIP 7 | selector: 8 | service: vt-styles 9 | ports: 10 | - port: 8083 11 | targetPort: 80 12 | --- 13 | apiVersion: apps/v1 14 | kind: Deployment 15 | metadata: 16 | name: vt-styles-deployment 17 | spec: 18 | replicas: 1 19 | selector: 20 | matchLabels: 21 | service: vt-styles 22 | template: 23 | metadata: 24 | labels: 25 | service: vt-styles 26 | tier: backend 27 | spec: 28 | containers: 29 | - name: vt-styles 30 | image: basisvisualisierung/vt-styles 31 | ports: 32 | - containerPort: 80 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vt-map-editor", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e" 11 | }, 12 | "private": true, 13 | "dependencies": { 14 | "@angular/animations": "^11.2.10", 15 | "@angular/cdk": "^11.2.9", 16 | "@angular/common": "^11.2.10", 17 | "@angular/compiler": "^11.2.10", 18 | "@angular/core": "^11.2.10", 19 | "@angular/forms": "^11.2.10", 20 | "@angular/material": "^11.2.9", 21 | "@angular/platform-browser": "^11.2.10", 22 | "@angular/platform-browser-dynamic": "^11.2.10", 23 | "@angular/router": "^11.2.10", 24 | "angular-resizable-element": "^3.3.5", 25 | "bootstrap": "^4.6.0", 26 | "color-convert": "^2.0.1", 27 | "maplibre-gl": "1.14.0", 28 | "material-design-icons": "^3.0.1", 29 | "ngx-clipboard": "^14.0.1", 30 | "ngx-color-picker": "^11.0.0", 31 | "rxjs": "~6.5.5", 32 | "tslib": "^2.1.0", 33 | "zone.js": "~0.10.2" 34 | }, 35 | "devDependencies": { 36 | "@angular-devkit/build-angular": "^0.1102.9", 37 | "@angular/cli": "^11.2.9", 38 | "@angular/compiler-cli": "^11.2.10", 39 | "@angular/language-service": "^11.2.10", 40 | "@compodoc/compodoc": "^1.1.11", 41 | "@types/jasmine": "~3.6.0", 42 | "@types/jasminewd2": "^2.0.8", 43 | "@types/node": "^14.14.41", 44 | "codelyzer": "^6.0.0", 45 | "jasmine-core": "~3.6.0", 46 | "jasmine-spec-reporter": "~5.0.0", 47 | "karma": "~6.3.2", 48 | "karma-chrome-launcher": "~3.1.0", 49 | "karma-coverage-istanbul-reporter": "~3.0.2", 50 | "karma-jasmine": "~4.0.0", 51 | "karma-jasmine-html-reporter": "^1.5.0", 52 | "protractor": "~7.0.0", 53 | "ts-node": "~7.0.0", 54 | "tslint": "^6.1.3", 55 | "typescript": "~4.1.5" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /projects/vt-map-view/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /projects/vt-map-view/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | browserName: 'chrome' 17 | }, 18 | directConnect: true, 19 | baseUrl: 'http://localhost:4200/', 20 | framework: 'jasmine', 21 | jasmineNodeOpts: { 22 | showColors: true, 23 | defaultTimeoutInterval: 30000, 24 | print: function() {} 25 | }, 26 | onPrepare() { 27 | require('ts-node').register({ 28 | project: require('path').join(__dirname, './tsconfig.json') 29 | }); 30 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 31 | } 32 | }; -------------------------------------------------------------------------------- /projects/vt-map-view/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('vt-map-view app is running!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /projects/vt-map-view/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText() { 9 | return element(by.css('app-root .content span')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /projects/vt-map-view/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es2018", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /projects/vt-map-view/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../../coverage/vt-map-view'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /projects/vt-map-view/src/app/app-config.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { SettingsModel } from './shared/settings.model'; 4 | 5 | /** 6 | * Central access to app configuration 7 | */ 8 | @Injectable() 9 | export class AppConfigService { 10 | static settings: SettingsModel; 11 | 12 | constructor(private http: HttpClient) { } 13 | 14 | /** 15 | * Read configuration from config.json 16 | */ 17 | initConfig() { 18 | return new Promise((resolve, reject) => { 19 | this.http.get('assets/config/config.json') 20 | .toPromise() 21 | .then((settings: SettingsModel) => { 22 | AppConfigService.settings = settings as SettingsModel; 23 | resolve(); 24 | }); 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /projects/vt-map-view/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /projects/vt-map-view/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | height: 100vh 3 | } 4 | -------------------------------------------------------------------------------- /projects/vt-map-view/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | 4 | describe('AppComponent', () => { 5 | beforeEach(waitForAsync(() => { 6 | TestBed.configureTestingModule({ 7 | declarations: [ 8 | AppComponent 9 | ], 10 | }).compileComponents(); 11 | })); 12 | 13 | it('should create the app', () => { 14 | const fixture = TestBed.createComponent(AppComponent); 15 | const app = fixture.debugElement.componentInstance; 16 | expect(app).toBeTruthy(); 17 | }); 18 | 19 | it(`should have as title 'vt-map-view'`, () => { 20 | const fixture = TestBed.createComponent(AppComponent); 21 | const app = fixture.debugElement.componentInstance; 22 | expect(app.title).toEqual('vt-map-view'); 23 | }); 24 | 25 | it('should render title', () => { 26 | const fixture = TestBed.createComponent(AppComponent); 27 | fixture.detectChanges(); 28 | const compiled = fixture.debugElement.nativeElement; 29 | expect(compiled.querySelector('.content span').textContent).toContain('vt-map-view app is running!'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /projects/vt-map-view/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.scss'] 7 | }) 8 | export class AppComponent { 9 | title = 'vt-map-view'; 10 | } 11 | -------------------------------------------------------------------------------- /projects/vt-map-view/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule, APP_INITIALIZER } from '@angular/core'; 3 | import { HttpClientModule } from '@angular/common/http'; 4 | import { Routes, RouterModule } from '@angular/router'; 5 | 6 | import { AppComponent } from './app.component'; 7 | import { AppConfigService } from './app-config.service'; 8 | import { MaplibreGlComponent } from './maplibre-gl/maplibre-gl.component'; 9 | 10 | const appRoutes: Routes = [ 11 | {path: ':id', component: MaplibreGlComponent} 12 | ]; 13 | 14 | /** 15 | * Read configuration during initialization of the app 16 | * @param appConfigService AppConfigService 17 | */ 18 | export function initConfig(appConfigService: AppConfigService) { 19 | return () => appConfigService.initConfig(); 20 | } 21 | 22 | @NgModule({ 23 | declarations: [ 24 | AppComponent, 25 | MaplibreGlComponent 26 | ], 27 | imports: [ 28 | BrowserModule, 29 | HttpClientModule, 30 | RouterModule.forRoot(appRoutes, { relativeLinkResolution: 'legacy' }) 31 | ], 32 | providers: [ 33 | AppConfigService, 34 | { 35 | provide: APP_INITIALIZER, 36 | useFactory: initConfig, 37 | multi: true, 38 | deps: [AppConfigService] 39 | } 40 | ], 41 | bootstrap: [AppComponent] 42 | }) 43 | export class AppModule { } 44 | -------------------------------------------------------------------------------- /projects/vt-map-view/src/app/maplibre-gl/maplibre-gl.component.scss: -------------------------------------------------------------------------------- 1 | .map { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | 6 | .mapboxgl-ctrl-group > button { 7 | width: 40px; 8 | height: 40px; 9 | 10 | .material-icons { 11 | font-size: 24px; 12 | } 13 | } 14 | 15 | .mapboxgl-ctrl-icon.mapboxgl-ctrl-compass > .mapboxgl-ctrl-compass-arrow { 16 | width: 24px; 17 | height: 24px; 18 | } 19 | 20 | .info-popup { 21 | max-height: 400px; 22 | 23 | .mapboxgl-popup-content { 24 | overflow: auto; 25 | padding-top: 30px; 26 | 27 | .mapboxgl-popup-close-button { 28 | font-size: 40px; 29 | position: fixed; 30 | margin: 10px 20px 0; 31 | background-color: #FFF; 32 | padding: 0; 33 | } 34 | 35 | .info-table { 36 | font-size: 15px; 37 | border-bottom: 1px solid grey; 38 | padding: 10px 0; 39 | } 40 | } 41 | } 42 | 43 | .searchInput { 44 | height: 50px; 45 | border: none; 46 | margin: 0; padding: 6px; 47 | font-size: 18px; 48 | 49 | &.hidden { 50 | display: none; 51 | } 52 | 53 | &:focus { 54 | outline: none; 55 | } 56 | } 57 | 58 | .resultArea { 59 | background-color: #FFF; 60 | width: 100%; 61 | 62 | &.hidden { 63 | display: none; 64 | } 65 | 66 | .result-row { 67 | height: 50px; 68 | font-size: 16px; 69 | max-width: 80vw; 70 | border-top: 1px solid lightgrey; 71 | padding: 5px 5px; 72 | 73 | &:hover { 74 | background-color: #e9efff; 75 | } 76 | 77 | .result-row-main { 78 | font-weight: bold; 79 | } 80 | } 81 | } 82 | 83 | .zoom-control { 84 | margin: 10px; 85 | padding: 5px; 86 | background-color: white; 87 | opacity: 0.8; 88 | font-size: 20px; 89 | 90 | @media (max-width: 576px) { 91 | display: none; 92 | } 93 | } 94 | 95 | .mapboxgl-ctrl-scale { 96 | @media (max-width: 576px) { 97 | display: none; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /projects/vt-map-view/src/app/maplibre-gl/maplibre-gl.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | 3 | import { MaplibreGlComponent } from './maplibre-gl.component'; 4 | 5 | describe('MaplibreGlComponent', () => { 6 | let component: MaplibreGlComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(waitForAsync(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [MaplibreGlComponent] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(MaplibreGlComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /projects/vt-map-view/src/app/maplibre-gl/maplibre-gl.component.ts: -------------------------------------------------------------------------------- 1 | import { AfterViewInit, Component, OnInit, ViewEncapsulation } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | 4 | import maplibregl from 'maplibre-gl'; 5 | 6 | import MaplibreGlPitchControl from 'src/app/map/maplibre-gl/maplibre-gl.pitch.control'; 7 | import MaplibreGlSearchControl from 'src/app/map/maplibre-gl/maplibre-gl.search.control'; 8 | import { AppConfigService } from '../app-config.service'; 9 | import { HttpClient, HttpHeaders } from '@angular/common/http'; 10 | 11 | /** 12 | * MapLibre GL JS map client 13 | */ 14 | @Component({ 15 | selector: 'app-maplibre-gl', 16 | template: '', 17 | styleUrls: ['./maplibre-gl.component.scss'], 18 | encapsulation: ViewEncapsulation.None 19 | }) 20 | export class MaplibreGlComponent implements OnInit { 21 | activeStyling: {}; 22 | map: maplibregl.Map; 23 | 24 | constructor(private http: HttpClient, 25 | private route: ActivatedRoute) { } 26 | 27 | ngOnInit() { 28 | // Set initial map parameter 29 | this.map = new maplibregl.Map({ 30 | container: 'map', 31 | }); 32 | const mapId = this.route.snapshot.params['id']; 33 | const options = { 34 | headers: new HttpHeaders({ 35 | 'Content-Type': 'application/json' 36 | }) 37 | }; 38 | this.http.get(AppConfigService.settings.mapService.url + '/config/' + mapId, options).subscribe((response: any) => { 39 | this.addControls(response); 40 | }); 41 | this.http.get(AppConfigService.settings.mapService.url + '/style/' + mapId, options).subscribe((response: any) => { 42 | // overwrite styling-response depending on url params 43 | if (/^(\d+\.?\d*)$/.test(this.route.snapshot.queryParamMap.get('zoom'))) { 44 | response.zoom = Number(this.route.snapshot.queryParamMap.get('zoom')); 45 | } 46 | if (/^(-?\d+\.?\d*)(,\s*-?\d+\.?\d*)$/.test(this.route.snapshot.queryParamMap.get('center'))) { 47 | response.center = this.route.snapshot.queryParamMap.get('center').split(',', 2).map(x => + x); 48 | } 49 | if (/^(\d+\.?\d*)$/.test(this.route.snapshot.queryParamMap.get('pitch'))) { 50 | response.pitch = Number(this.route.snapshot.queryParamMap.get('pitch')); 51 | } 52 | if (/^(-?(\d+\.?\d*))$/.test(this.route.snapshot.queryParamMap.get('bearing'))) { 53 | response.bearing = Number(this.route.snapshot.queryParamMap.get('bearing')); 54 | } 55 | // set active styling 56 | this.activeStyling = response; 57 | this.map.setStyle(this.activeStyling); 58 | }); 59 | 60 | } 61 | 62 | /** 63 | * Adds controls to the map, depending on the configuration 64 | * @param config map configuration 65 | */ 66 | addControls(config: any) { 67 | this.map.addControl(new maplibregl.ScaleControl()); 68 | 69 | if (config.navigation.show && config.navigation.enabled) { 70 | this.map.addControl(new maplibregl.NavigationControl(), 'top-left'); 71 | this.map.addControl(new MaplibreGlPitchControl(), 'top-left'); 72 | this.map.addControl(new maplibregl.GeolocateControl({ 73 | positionOptions: { 74 | enableHighAccuracy: true 75 | } 76 | }), 'top-left'); 77 | } 78 | 79 | // Custom search control 80 | if (config.search.show && config.search.enabled) { 81 | this.map.addControl(new MaplibreGlSearchControl(AppConfigService.settings.mapService.url), 'top-right'); 82 | } 83 | 84 | // Popup 85 | this.map.on('click', (event: any) => { 86 | if (config.info.enabled) { 87 | const features = this.map.queryRenderedFeatures(event.point); 88 | if (features.length) { 89 | let content = ''; 90 | 91 | for (const feature of features) { 92 | content += '' + 93 | feature.layer.id + ''; 94 | for (const key in feature.properties) { 95 | if (feature.properties.hasOwnProperty(key)) { 96 | content += '' + key + ':' + feature.properties[key] + ''; 97 | } 98 | } 99 | content += ''; 100 | } 101 | 102 | new maplibregl.Popup({ 103 | className: 'info-popup', 104 | maxWidth: '400px' 105 | }) 106 | .setLngLat(event.lngLat) 107 | .setHTML(content) 108 | .addTo(this.map); 109 | } 110 | } 111 | }); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /projects/vt-map-view/src/app/shared/settings.model.ts: -------------------------------------------------------------------------------- 1 | import { MapFunctions } from 'src/app/shared/map-functions'; 2 | import { Basemap } from 'src/app/shared/basemap'; 3 | 4 | /** 5 | * Model of setting parameters 6 | */ 7 | export interface SettingsModel { 8 | mapService: { 9 | url: string; 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /projects/vt-map-view/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Basisvisualisierung/vt-map-editor/20daf75105c50a236f4cd4664c7c28445131bfcc/projects/vt-map-view/src/assets/.gitkeep -------------------------------------------------------------------------------- /projects/vt-map-view/src/assets/config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "mapService": { 3 | "url": "/map-service" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /projects/vt-map-view/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /projects/vt-map-view/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /projects/vt-map-view/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Basisvisualisierung/vt-map-editor/20daf75105c50a236f4cd4664c7c28445131bfcc/projects/vt-map-view/src/favicon.ico -------------------------------------------------------------------------------- /projects/vt-map-view/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | VT Map View 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /projects/vt-map-view/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /projects/vt-map-view/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags.ts'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | -------------------------------------------------------------------------------- /projects/vt-map-view/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /projects/vt-map-view/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /projects/vt-map-view/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "src/main.ts", 9 | "src/polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ], 14 | "exclude": [ 15 | "src/test.ts", 16 | "src/**/*.spec.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /projects/vt-map-view/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /projects/vt-map-view/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/app-config.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { SettingsModel } from './shared/settings.model'; 4 | 5 | /** 6 | * Central access to app configuration 7 | */ 8 | @Injectable({providedIn: 'root'}) 9 | export class AppConfigService { 10 | settings: SettingsModel; 11 | constructor(private http: HttpClient) { } 12 | 13 | /** 14 | * Read configuration from config.json 15 | */ 16 | initConfig() { 17 | return new Promise((resolve, reject) => { 18 | this.http.get('assets/config/config.json') 19 | .toPromise() 20 | .then((settings: SettingsModel) => { 21 | this.settings = settings as SettingsModel; 22 | resolve(); 23 | }); 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | @import '../styles.scss'; 2 | 3 | .mat-drawer-container { 4 | position: absolute; 5 | top: 56px; 6 | left: 0; 7 | right: 0; 8 | bottom: 0; 9 | } 10 | 11 | mat-sidenav-content { 12 | height: 100%; 13 | } 14 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | import { RouterTestingModule } from '@angular/router/testing'; 4 | import {MaplibreGlComponent} from './map/maplibre-gl/maplibre-gl.component'; 5 | import {MaterialDesignModule} from './material-design/material-design.module'; 6 | import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; 7 | import {HeaderComponent} from './header/header.component'; 8 | import {MenuComponent} from './menu/menu.component'; 9 | import {AppConfigService} from './app-config.service'; 10 | import {HeaderService} from './header/header.service'; 11 | import {EventEmitter} from '@angular/core'; 12 | 13 | describe('AppComponent', () => { 14 | beforeEach(waitForAsync(() => { 15 | TestBed.configureTestingModule({ 16 | imports: [ 17 | RouterTestingModule, 18 | MaterialDesignModule, 19 | BrowserAnimationsModule, 20 | ], 21 | declarations: [ 22 | AppComponent, 23 | MaplibreGlComponent, 24 | HeaderComponent, 25 | MenuComponent 26 | ], 27 | providers: [ 28 | {provide: AppConfigService, useClass: AppConfigServiceStub}, 29 | {provide: HeaderService, useClass: HeaderServiceStub} 30 | ], 31 | }).compileComponents(); 32 | })); 33 | 34 | it('should create the app', () => { 35 | const fixture = TestBed.createComponent(AppComponent); 36 | const app = fixture.debugElement.componentInstance; 37 | expect(app).toBeTruthy(); 38 | }); 39 | 40 | it(`should have as title 'VT Map Editor'`, () => { 41 | const fixture = TestBed.createComponent(AppComponent); 42 | fixture.detectChanges(); 43 | const app = fixture.debugElement.componentInstance; 44 | expect(app.appTitle).toEqual('VT Map Editor'); 45 | }); 46 | 47 | }); 48 | 49 | class AppConfigServiceStub{ 50 | settings = {menuItems: [ 51 | { 52 | label: 'Map', 53 | icon: 'map', 54 | link: 'map', 55 | externalLink: false 56 | }, 57 | { 58 | label: 'Privacy', 59 | icon: 'lock', 60 | link: 'privacy', 61 | externalLink: false 62 | }, 63 | { 64 | label: 'Legals', 65 | icon: 'comment', 66 | link: 'https://...', 67 | externalLink: true 68 | } 69 | ], 70 | 71 | }; 72 | } 73 | 74 | class HeaderServiceStub{ 75 | titleChanged = new EventEmitter(); 76 | changeTitle(title: string) { 77 | this.titleChanged.emit(''); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; 2 | import { MenuItem } from './menu/menu-item'; 3 | import { Router } from '@angular/router'; 4 | import { MatSidenav } from '@angular/material/sidenav'; 5 | 6 | /** 7 | * App component 8 | */ 9 | @Component({ 10 | selector: 'app-root', 11 | templateUrl: './app.component.html', 12 | styleUrls: ['./app.component.scss'] 13 | }) 14 | export class AppComponent implements OnInit { 15 | 16 | appTitle: string; 17 | version: string; 18 | @ViewChild('sidemenu') sidemenu: MatSidenav; 19 | 20 | constructor(public vcRef: ViewContainerRef, private router: Router) { } 21 | 22 | ngOnInit() { 23 | this.appTitle = 'VT Map Editor'; 24 | this.version = 'v2.0.0'; 25 | } 26 | 27 | /** 28 | * Item in sidebar menu selected 29 | * @param item MenuItem 30 | */ 31 | onItemSelected(item: MenuItem) { 32 | if (item.externalLink) { 33 | window.open(item.link, '_blank'); 34 | } else { 35 | this.router.navigate([item.link]); 36 | } 37 | this.sidemenu.close(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule, APP_INITIALIZER, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 3 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 4 | import { MaterialDesignModule } from './material-design/material-design.module'; 5 | import { Routes, RouterModule } from '@angular/router'; 6 | import { HttpClientModule } from '@angular/common/http'; 7 | import { ResizableModule } from 'angular-resizable-element'; 8 | 9 | import { ColorPickerModule } from 'ngx-color-picker'; 10 | import { ClipboardModule } from 'ngx-clipboard'; 11 | 12 | import { AppComponent } from './app.component'; 13 | import { HeaderComponent } from './header/header.component'; 14 | import { MenuComponent } from './menu/menu.component'; 15 | import { InfoComponent } from './info/info.component'; 16 | import { MapComponent } from './map/map.component'; 17 | import { ToolOverlayComponent } from './map/tools/tool-overlay/tool-overlay.component'; 18 | import { ToolBasemapComponent } from './map/tools/tool-basemap/tool-basemap.component'; 19 | import { ToolEditComponent } from './map/tools/tool-edit/tool-edit.component'; 20 | import { ToolFunctionsComponent } from './map/tools/tool-functions/tool-functions.component'; 21 | import { ToolShareComponent } from './map/tools/tool-share/tool-share.component'; 22 | import { ToolPrintEditorComponent } from './map/tools/tool-print-editor/tool-print-editor.component'; 23 | import { FeedbackComponent } from './feedback/feedback.component'; 24 | import { MaplibreGlComponent } from './map/maplibre-gl/maplibre-gl.component'; 25 | import { LayerElementComponent } from './map/tools/tool-edit/layer/layer-element.component'; 26 | import { LayerConfigurationComponent } from './map/tools/tool-edit/layer/layer-configuration.component'; 27 | import { GroupConfigurationComponent } from './map/tools/tool-edit/group-configuration/group-configuration.component'; 28 | import { GuiLayerConfigurationComponent } from './map/tools/tool-edit/gui-layer/gui-layer-configuration.component'; 29 | import { GuiLayerElementComponent } from './map/tools/tool-edit/gui-layer/gui-layer-element.component'; 30 | 31 | import { HeaderService } from './header/header.service'; 32 | import { MapStylingService } from './map/map-styling.service'; 33 | import { MapFunctionService } from './map/map-function.service'; 34 | import { AppConfigService } from './app-config.service'; 35 | 36 | const appRoutes: Routes = [ 37 | {path: '', redirectTo: 'map', pathMatch: 'full'}, 38 | {path: 'map', component: MapComponent, children: [ 39 | {path: 'basemap', component: ToolBasemapComponent}, 40 | {path: 'edit', component: ToolEditComponent, children: [ 41 | {path: 'group-layer', component: GroupConfigurationComponent}, 42 | {path: 'gui-layer', component: GuiLayerConfigurationComponent}, 43 | {path: 'layer', component: LayerConfigurationComponent} 44 | ]}, 45 | {path: 'functions', component: ToolFunctionsComponent}, 46 | {path: 'share', component: ToolShareComponent} 47 | ]}, 48 | {path: 'privacy', component: InfoComponent}, 49 | {path: 'legals', component: InfoComponent}, 50 | {path: 'feedback', component: FeedbackComponent} 51 | ]; 52 | 53 | /** 54 | * Read configuration during initialization of the app 55 | * @param appConfigService AppConfigService 56 | */ 57 | export function initConfig(appConfigService: AppConfigService) { 58 | return () => appConfigService.initConfig(); 59 | } 60 | 61 | @NgModule({ 62 | declarations: [ 63 | AppComponent, 64 | HeaderComponent, 65 | MenuComponent, 66 | MapComponent, 67 | ToolOverlayComponent, 68 | ToolBasemapComponent, 69 | ToolEditComponent, 70 | ToolFunctionsComponent, 71 | ToolShareComponent, 72 | ToolPrintEditorComponent, 73 | FeedbackComponent, 74 | MaplibreGlComponent, 75 | LayerElementComponent, 76 | GroupConfigurationComponent, 77 | GuiLayerConfigurationComponent, 78 | GuiLayerElementComponent, 79 | InfoComponent, 80 | LayerConfigurationComponent 81 | ], 82 | imports: [ 83 | BrowserModule, 84 | BrowserAnimationsModule, 85 | HttpClientModule, 86 | MaterialDesignModule, 87 | ResizableModule, 88 | RouterModule.forRoot(appRoutes, { relativeLinkResolution: 'legacy' }), 89 | ColorPickerModule, 90 | ClipboardModule 91 | ], 92 | providers: [ 93 | AppConfigService, 94 | { 95 | provide: APP_INITIALIZER, 96 | useFactory: initConfig, 97 | multi: true, 98 | deps: [AppConfigService] 99 | }, 100 | HeaderService, 101 | MapStylingService, 102 | MapFunctionService 103 | ], 104 | exports: [ 105 | HeaderComponent, 106 | AppComponent, 107 | MaplibreGlComponent, 108 | HeaderComponent, 109 | MenuComponent, 110 | MaterialDesignModule, 111 | ColorPickerModule, 112 | ], 113 | bootstrap: [AppComponent], 114 | schemas: [CUSTOM_ELEMENTS_SCHEMA] 115 | }) 116 | 117 | /** 118 | * App module 119 | */ 120 | export class AppModule { } 121 | -------------------------------------------------------------------------------- /src/app/feedback/feedback.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{topic}} 5 | 6 | 7 | 8 | 9 | star_border 10 | 11 | 12 | 13 | 14 | 15 | 16 | Kommentar 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/app/feedback/feedback.component.scss: -------------------------------------------------------------------------------- 1 | .star-row { 2 | mat-icon { 3 | font-size: 60px; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/app/feedback/feedback.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { FeedbackComponent } from './feedback.component'; 3 | import {HeaderService} from '../header/header.service'; 4 | import {RouterTestingModule} from '@angular/router/testing'; 5 | import {MaterialDesignModule} from '../material-design/material-design.module'; 6 | import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; 7 | import {HttpClientTestingModule} from '@angular/common/http/testing'; 8 | 9 | describe('FeedbackComponent', () => { 10 | let component: FeedbackComponent; 11 | let fixture: ComponentFixture; 12 | 13 | beforeEach(waitForAsync(() => { 14 | TestBed.configureTestingModule({ 15 | imports: [ 16 | RouterTestingModule, 17 | MaterialDesignModule, 18 | BrowserAnimationsModule, 19 | HttpClientTestingModule 20 | ], 21 | declarations: [ 22 | FeedbackComponent 23 | ], 24 | providers: [ 25 | { provide: HeaderService, useClass: HeaderServiceStub } 26 | ] 27 | }) 28 | .compileComponents(); 29 | })); 30 | 31 | beforeEach(() => { 32 | fixture = TestBed.createComponent(FeedbackComponent); 33 | component = fixture.componentInstance; 34 | fixture.detectChanges(); 35 | }); 36 | 37 | it('should create', () => { 38 | expect(component).toBeTruthy(); 39 | }); 40 | }); 41 | 42 | class HeaderServiceStub{ 43 | changeTitle(title: string) {} 44 | } 45 | -------------------------------------------------------------------------------- /src/app/feedback/feedback.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { HeaderService } from '../header/header.service'; 3 | /** 4 | * Component for user feedback 5 | */ 6 | @Component({ 7 | selector: 'app-feedback', 8 | templateUrl: './feedback.component.html', 9 | styleUrls: ['./feedback.component.scss'] 10 | }) 11 | export class FeedbackComponent implements OnInit { 12 | ratingTopics = [ 13 | 'Layout', 14 | 'Bedienbarkeit', 15 | 'Kartenstyle', 16 | 'Funktionen' 17 | ]; 18 | 19 | constructor(private headerService: HeaderService) { } 20 | 21 | ngOnInit() { 22 | // Change header Title 23 | this.headerService.changeTitle('Bewertung'); 24 | } 25 | 26 | /** 27 | * A rating star was selected 28 | * @param event Event 29 | */ 30 | onSelectStar(event: any) { 31 | console.log(event.target.parentElement.parentElement); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/app/header/header.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | menu 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/app/header/header.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles.scss'; 2 | 3 | .header { 4 | background-color: #FFFFFF; 5 | box-shadow: 0 0 10px grey; 6 | height: $header-height; 7 | font-weight: bold; 8 | position: relative; 9 | z-index: 4; 10 | 11 | .header-title { 12 | width: 100%; 13 | margin: -40px; 14 | text-align: center 15 | } 16 | 17 | .menu-btn { 18 | mat-icon { 19 | font-size: 30px; 20 | width: 30px; 21 | height: 25px; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/app/header/header.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { HeaderComponent } from './header.component'; 3 | import {HeaderService} from './header.service'; 4 | import {RouterTestingModule} from '@angular/router/testing'; 5 | import {EventEmitter} from '@angular/core'; 6 | import {MaterialDesignModule} from '../material-design/material-design.module'; 7 | import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; 8 | import {HttpClientTestingModule} from '@angular/common/http/testing'; 9 | import {By} from '@angular/platform-browser'; 10 | 11 | describe('HeaderComponent', () => { 12 | let component: HeaderComponent; 13 | let fixture: ComponentFixture; 14 | let headerService: HeaderServiceStub; 15 | 16 | beforeEach(waitForAsync(() => { 17 | TestBed.configureTestingModule({ 18 | declarations: [HeaderComponent], 19 | imports: [ 20 | RouterTestingModule, 21 | MaterialDesignModule, 22 | BrowserAnimationsModule, 23 | HttpClientTestingModule 24 | ], 25 | providers: [ 26 | {provide: HeaderService, useClass: HeaderServiceStub} 27 | ] 28 | }) 29 | .compileComponents(); 30 | })); 31 | 32 | beforeEach(() => { 33 | fixture = TestBed.createComponent(HeaderComponent); 34 | component = fixture.componentInstance; 35 | fixture.detectChanges(); 36 | }); 37 | 38 | it('should create', () => { 39 | expect(component).toBeTruthy(); 40 | }); 41 | 42 | it('should contain a header-title tag', () => { 43 | expect(fixture.debugElement.query(By.css('.header-title'))).toBeTruthy(); 44 | }); 45 | 46 | it('should have a header title', () => { 47 | component.title = 'Test Title'; 48 | fixture.detectChanges(); 49 | expect(fixture.debugElement.query(By.css('.header-title')).nativeElement.textContent).toBe('Test Title'); 50 | }); 51 | 52 | it('should render a title emitted by headerService', fakeAsync(() => { 53 | headerService = TestBed.inject(HeaderService) as HeaderServiceStub; 54 | headerService.titleChanged.emit('a title from headerService'); 55 | fixture.detectChanges(); 56 | expect(fixture.debugElement.query(By.css('.header-title')).nativeElement.textContent).toBe('a title from headerService'); 57 | })); 58 | 59 | it('should have a button of class menu-btn', () => { 60 | expect(fixture.debugElement.query(By.css('.menu-btn'))).toBeTruthy(); 61 | }); 62 | 63 | it('should emit toggleMenu on menu-btn click', () => { 64 | spyOn(component.toggleMenu, 'emit'); 65 | const button = fixture.nativeElement.querySelector('.menu-btn'); 66 | button.dispatchEvent(new Event('click')); 67 | fixture.detectChanges(); 68 | expect(component.toggleMenu.emit).toHaveBeenCalledTimes(1); 69 | }); 70 | }); 71 | 72 | class HeaderServiceStub{ 73 | titleChanged = new EventEmitter(); 74 | changeTitle(title: string) { 75 | this.titleChanged.emit(title); 76 | } 77 | } 78 | 79 | -------------------------------------------------------------------------------- /src/app/header/header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; 2 | import { HeaderService } from './header.service'; 3 | 4 | /** 5 | * App header 6 | */ 7 | @Component({ 8 | selector: 'app-header', 9 | templateUrl: './header.component.html', 10 | styleUrls: ['./header.component.scss'] 11 | }) 12 | export class HeaderComponent implements OnInit { 13 | @Output() toggleMenu = new EventEmitter(); 14 | title: string; 15 | 16 | constructor(private headerService: HeaderService) {} 17 | 18 | ngOnInit() { 19 | this.headerService.titleChanged.subscribe( 20 | (title) => { 21 | this.title = title; 22 | } 23 | ); 24 | } 25 | 26 | /** 27 | * Menu button clicked 28 | * @emits HeaderComponent#toggleMenu 29 | */ 30 | onMenuClicked() { 31 | this.toggleMenu.emit(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/app/header/header.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { HeaderService } from './header.service'; 4 | import {HttpClientTestingModule} from '@angular/common/http/testing'; 5 | import {AppConfigService} from '../app-config.service'; 6 | 7 | 8 | describe('HeaderService', () => { 9 | let headerService: HeaderService; 10 | 11 | beforeEach(() => { 12 | TestBed.configureTestingModule({ 13 | imports: [HttpClientTestingModule], 14 | providers: [HeaderService, 15 | {provide: AppConfigService, useClass: AppConfigServiceStub}, 16 | ] 17 | }); 18 | }); 19 | 20 | it('should be created', () => { 21 | headerService = TestBed.inject(HeaderService); 22 | expect(headerService).toBeTruthy(); 23 | }); 24 | }); 25 | class AppConfigServiceStub{} 26 | -------------------------------------------------------------------------------- /src/app/header/header.service.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter, Injectable } from '@angular/core'; 2 | 3 | /** 4 | * Service for changing the header title 5 | */ 6 | @Injectable() 7 | export class HeaderService { 8 | title: string; 9 | titleChanged = new EventEmitter(); 10 | 11 | constructor() { } 12 | 13 | /** 14 | * Changes the header title 15 | * @param title Title to be shown in the header 16 | * @emits HeaderService#titleChanged 17 | */ 18 | changeTitle(title: string) { 19 | this.title = title; 20 | this.titleChanged.emit(this.title); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/info/info.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/info/info.component.scss: -------------------------------------------------------------------------------- 1 | app-info { 2 | .info-content { 3 | margin: 20px; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/app/info/info.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | 3 | import { InfoComponent } from './info.component'; 4 | import {RouterTestingModule} from '@angular/router/testing'; 5 | import {HttpClientTestingModule} from '@angular/common/http/testing'; 6 | import {HeaderService} from '../header/header.service'; 7 | import {MenuComponent} from '../menu/menu.component'; 8 | import {ActivatedRoute} from '@angular/router'; 9 | import {ActivatedRouteStub} from '../../testing/activated-route-stub'; 10 | import {MaterialDesignModule} from '../material-design/material-design.module'; 11 | import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; 12 | 13 | 14 | 15 | describe('InfoComponent', () => { 16 | let component: InfoComponent; 17 | let fixture: ComponentFixture; 18 | let activatedRoute: ActivatedRouteStub; 19 | 20 | beforeEach(waitForAsync(() => { 21 | activatedRoute = new ActivatedRouteStub({snapshot: {params: {myId: '123'}}}); 22 | TestBed.configureTestingModule({ 23 | declarations: [InfoComponent, MenuComponent], 24 | imports: [ 25 | RouterTestingModule, 26 | MaterialDesignModule, 27 | BrowserAnimationsModule, 28 | HttpClientTestingModule 29 | ], 30 | providers: [ 31 | {provide: ActivatedRoute, useValue: {snapshot: {url: '123'}}}, 32 | HeaderService, 33 | ] 34 | }).compileComponents(); 35 | })); 36 | 37 | beforeEach(() => { 38 | fixture = TestBed.createComponent(InfoComponent); 39 | component = fixture.componentInstance; 40 | fixture.detectChanges(); 41 | }); 42 | 43 | it('should create', () => { 44 | expect(component).toBeTruthy(); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/app/info/info.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild, ElementRef, ViewEncapsulation, AfterViewInit } from '@angular/core'; 2 | import { ActivatedRoute, Router } from '@angular/router'; 3 | import { HttpClient } from '@angular/common/http'; 4 | import { HeaderService } from '../header/header.service'; 5 | 6 | /** 7 | * Component for information about legals, contact, privacy etc. 8 | */ 9 | @Component({ 10 | selector: 'app-info', 11 | templateUrl: './info.component.html', 12 | styleUrls: ['./info.component.scss'], 13 | encapsulation: ViewEncapsulation.None 14 | }) 15 | export class InfoComponent implements OnInit, AfterViewInit { 16 | 17 | @ViewChild('content') content: ElementRef; 18 | 19 | constructor(private route: ActivatedRoute, 20 | private router: Router, 21 | private http: HttpClient, 22 | private headerService: HeaderService) { } 23 | 24 | ngOnInit() { } 25 | 26 | ngAfterViewInit() { 27 | const path = this.route.snapshot.url[0].path; 28 | let file = ''; 29 | let header = ''; 30 | if (path === 'privacy') { 31 | file = 'privacy.html'; 32 | header = 'Datenschutz'; 33 | } else if (path === 'legals') { 34 | file = 'legals.html'; 35 | header = 'Impressum'; 36 | } 37 | 38 | this.http.get('assets/templates/' + file, {responseType: 'text'}).subscribe((data) => { 39 | this.headerService.changeTitle(header); 40 | this.content.nativeElement.innerHTML = data; 41 | }); 42 | 43 | } 44 | 45 | onCloseInfo() { 46 | this.router.navigate(['map']); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/app/map/map-function.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { MapFunctionService } from './map-function.service'; 4 | import {AppConfigService} from '../app-config.service'; 5 | import {HttpClientTestingModule} from '@angular/common/http/testing'; 6 | 7 | describe('MapFunctionService', () => { 8 | let mapFunctionService: MapFunctionService; 9 | 10 | beforeEach(() => TestBed.configureTestingModule({ 11 | imports: [HttpClientTestingModule], 12 | providers: [ 13 | MapFunctionService, 14 | {provide: AppConfigService, useClass: AppConfigServiceStub} 15 | ] 16 | })); 17 | 18 | it('should be created', () => { 19 | mapFunctionService = TestBed.inject(MapFunctionService); 20 | expect(mapFunctionService).toBeTruthy(); 21 | }); 22 | 23 | describe('toggleMapFunction', () => { 24 | beforeEach(() => { 25 | mapFunctionService = TestBed.inject(MapFunctionService); 26 | spyOn(mapFunctionService.mapFunctionsChanged, 'emit'); 27 | mapFunctionService.toggleMapFunction('search', true); 28 | }); 29 | 30 | it('should emit if called', () => { 31 | expect(mapFunctionService.mapFunctionsChanged.emit).toHaveBeenCalledTimes(1); 32 | }); 33 | 34 | it('should emit function name', () => { 35 | expect(mapFunctionService.mapFunctionsChanged.emit).toHaveBeenCalledWith('search'); 36 | }); 37 | 38 | it('should have a enabled value for this map function that is true', () => { 39 | expect(mapFunctionService.mapFunctions.search.enabled).toBeTruthy(); 40 | }); 41 | 42 | it('should have a enabled value for this map function that is false, if called with enabled = false', () => { 43 | mapFunctionService.toggleMapFunction('search', false); 44 | expect(mapFunctionService.mapFunctions.search.enabled).toBeFalsy(); 45 | }); 46 | }); 47 | 48 | describe('setFunctionConfiguration', () => { 49 | beforeEach(() => { 50 | mapFunctionService = TestBed.inject(MapFunctionService); 51 | spyOn(mapFunctionService.mapFunctionsChanged, 'emit'); 52 | mapFunctionService.setFunctionConfiguration('search', 'test'); 53 | }); 54 | 55 | it('should emit if called', () => { 56 | expect(mapFunctionService.mapFunctionsChanged.emit).toHaveBeenCalledTimes(1); 57 | }); 58 | 59 | it('should emit function name', () => { 60 | expect(mapFunctionService.mapFunctionsChanged.emit).toHaveBeenCalledWith('search'); 61 | }); 62 | 63 | it('should have a configuration value after called', () => { 64 | expect(mapFunctionService.mapFunctions.search.configuration).toBeTruthy(); 65 | }); 66 | 67 | it('should have "Test" configuration value after called', () => { 68 | expect(mapFunctionService.mapFunctions.search.configuration).toEqual('test'); 69 | }); 70 | }); 71 | 72 | }); 73 | class AppConfigServiceStub { 74 | settings = { 75 | mapFunctions: { 76 | navigation: { 77 | show: true, 78 | enabled: true 79 | }, 80 | info: { 81 | show: true, 82 | enabled: true 83 | }, 84 | search: { 85 | show: true, 86 | enabled: true 87 | } 88 | }, 89 | }; 90 | } 91 | -------------------------------------------------------------------------------- /src/app/map/map-function.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable, EventEmitter, OnInit} from '@angular/core'; 2 | import { MapFunctions } from '../shared/map-functions'; 3 | import { AppConfigService } from '../app-config.service'; 4 | 5 | /** 6 | * Service to manage the map functions 7 | */ 8 | @Injectable() 9 | export class MapFunctionService { 10 | mapFunctions: MapFunctions; 11 | mapFunctionsChanged = new EventEmitter(); 12 | metadataChanged = new EventEmitter(); 13 | // tslint:disable-next-line:variable-name 14 | guiLayerState: boolean; 15 | // tslint:disable-next-line:variable-name 16 | groupLayerState: boolean; 17 | 18 | constructor(private appConfigService: AppConfigService) { 19 | this.mapFunctions = appConfigService.settings.mapFunctions; 20 | } 21 | 22 | /** 23 | * (De-)activates a map function 24 | * @param functionName Name of the map function 25 | * @param enable true: activate; false: deactivate 26 | */ 27 | toggleMapFunction(functionName: string, enable) { 28 | this.mapFunctions[functionName].enabled = enable; 29 | this.mapFunctionsChanged.emit(functionName); 30 | } 31 | 32 | /** 33 | * Set the configuration for a map function 34 | * @param functionName Name of the map function 35 | * @param config configuration object 36 | */ 37 | setFunctionConfiguration(functionName: string, config: any) { 38 | this.mapFunctions[functionName].configuration = config; 39 | this.mapFunctionsChanged.emit(functionName); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/app/map/map-saving.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { MapFunctionService } from './map-function.service'; 2 | import { MapStylingService } from 'src/app/map/map-styling.service'; 3 | import { ActivatedRoute } from '@angular/router'; 4 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 5 | import { TestBed } from '@angular/core/testing'; 6 | import { MapSavingService } from './map-saving.service'; 7 | 8 | describe('MapSavingService', () => { 9 | 10 | let mapSavingService: MapSavingService; 11 | 12 | beforeEach(() => { 13 | TestBed.configureTestingModule({ 14 | imports: [HttpClientTestingModule], 15 | providers: [ 16 | { 17 | provide: MapStylingService, 18 | useValue: { 19 | basemapse: [] 20 | } 21 | }, 22 | { 23 | provide: MapFunctionService, 24 | useValue: { 25 | mapFunctions: [] 26 | } 27 | }, 28 | { 29 | provide: ActivatedRoute, 30 | useValue: { 31 | snapshot: { 32 | queryParamMap: { 33 | get: () => { 34 | return []; 35 | }, 36 | }, 37 | }, 38 | }, 39 | } 40 | ] 41 | }); 42 | mapSavingService = TestBed.inject(MapSavingService); 43 | }); 44 | 45 | it('should be created', () => { 46 | expect(mapSavingService).toBeTruthy(); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/app/map/map-saving.service.ts: -------------------------------------------------------------------------------- 1 | import { MapFunctionService } from './map-function.service'; 2 | import { AppConfigService } from './../app-config.service'; 3 | import { MapStylingService } from 'src/app/map/map-styling.service'; 4 | import { HttpHeaders, HttpClient } from '@angular/common/http'; 5 | import { Injectable } from '@angular/core'; 6 | import { Observable } from 'rxjs'; 7 | 8 | /** 9 | * Service for saving a map configuration 10 | */ 11 | @Injectable({ 12 | providedIn: 'root' 13 | }) 14 | export class MapSavingService { 15 | 16 | constructor( 17 | private mapStylingService: MapStylingService, 18 | private appConfigService: AppConfigService, 19 | private mapFunctionService: MapFunctionService, 20 | private http: HttpClient 21 | ) { } 22 | 23 | /** 24 | * Saves the current map configuration 25 | * @returns a http request which returns a configuration id on successful saving 26 | */ 27 | saveMap(): Observable { 28 | const options = { 29 | headers: new HttpHeaders({ 30 | 'Content-Type': 'application/json' 31 | }) 32 | }; 33 | // Add map view parameters (zoom, center, ...) 34 | const mapView = this.mapStylingService.mapView; 35 | for (const key in mapView) { 36 | if (mapView.hasOwnProperty(key)) { 37 | this.mapStylingService.activeStyling[key] = mapView[key]; 38 | } 39 | } 40 | const data = { 41 | style: this.mapStylingService.activeStyling, 42 | configuration: this.mapFunctionService.mapFunctions 43 | }; 44 | return this.http.post(this.appConfigService.settings.mapService.url + '/map', data, options); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/app/map/map.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | 14 | {{tool.icon}} 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/app/map/map.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles.scss'; 2 | 3 | .toolbar { 4 | position: absolute; 5 | bottom: 10px; 6 | text-align: center; 7 | width: 100%; 8 | 9 | .toolbar-btn { 10 | display: inline-block; 11 | 12 | button { 13 | background-color: #FFFFFF; 14 | margin: 0 10px; 15 | height: 60px; 16 | width: 60px; 17 | 18 | mat-icon { 19 | font-size: 30px; 20 | width: 30px; 21 | height: 30px; 22 | } 23 | } 24 | } 25 | } 26 | 27 | .map { 28 | width: 100%; 29 | height: 100%; 30 | background-color: #ece3c3; 31 | } 32 | -------------------------------------------------------------------------------- /src/app/map/map.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnDestroy, OnInit} from '@angular/core'; 2 | import { MapTool } from './tools/map-tool'; 3 | import {Router, ActivatedRoute, NavigationStart} from '@angular/router'; 4 | import { HeaderService } from '../header/header.service'; 5 | import { AppConfigService } from '../app-config.service'; 6 | import { MapStylingService } from './map-styling.service'; 7 | import {MapFunctionService} from './map-function.service'; 8 | 9 | /** 10 | * Map component containing map client, toolbar and tool overlay 11 | */ 12 | @Component({ 13 | selector: 'app-map', 14 | templateUrl: './map.component.html', 15 | styleUrls: ['./map.component.scss'] 16 | }) 17 | export class MapComponent implements OnInit, OnDestroy { 18 | tools: MapTool[] = [ 19 | new MapTool( 20 | 'map', 21 | 'basemap' 22 | ), 23 | new MapTool( 24 | 'layers', 25 | 'edit' 26 | ), 27 | new MapTool( 28 | 'settings', 29 | 'functions' 30 | ), 31 | new MapTool( 32 | 'share', 33 | 'share' 34 | ) 35 | ]; 36 | 37 | showToolOverlay: boolean; 38 | toolOverlayHeight: number; 39 | toolOverlayMinHeight: number; 40 | headerTitle: string; 41 | activeChild: number; 42 | hasGuiLayers: boolean; 43 | showGroupConfiguration: boolean; 44 | showGuiLayerConfiguration: boolean; 45 | activeStyling: any; 46 | activeStylingChangedSubscription: any; 47 | activeBasemapChangedSubscription: any; 48 | 49 | constructor(private router: Router, 50 | private route: ActivatedRoute, 51 | private headerService: HeaderService, 52 | private mapStylingService: MapStylingService, 53 | private appConfigService: AppConfigService, 54 | private mapFunctionService: MapFunctionService) { } 55 | 56 | ngOnInit() { 57 | this.toolOverlayHeight = 300; 58 | this.toolOverlayMinHeight = 300; 59 | this.showToolOverlay = false; 60 | // Header title 61 | this.headerTitle = this.appConfigService.settings.titles.map; 62 | this.headerService.changeTitle(this.headerTitle); 63 | if (this.route.children.length !== 0) { 64 | this.showToolOverlay = true; 65 | } 66 | this.router.events 67 | .subscribe((event: NavigationStart) => { 68 | if (this.route.children.length !== 0) { 69 | this.showToolOverlay = true; 70 | } 71 | }); 72 | this.activeStylingChangedSubscription = this.mapStylingService.activeStylingChanged 73 | .subscribe(() => { 74 | this.activeStyling = this.mapStylingService.activeStyling; 75 | this.parseMetadata(); 76 | this.activeStylingChangedSubscription.unsubscribe(); 77 | }); 78 | this.activeBasemapChangedSubscription = this.mapStylingService.activeBasemapChanged.subscribe(() => { 79 | this.activeStyling = this.mapStylingService.activeStyling; 80 | this.parseMetadata(); 81 | }); 82 | 83 | // Load map by uuid or style if in print editor mode from query parameters 84 | const mapUuid = this.route.snapshot.queryParamMap.get('id'); 85 | 86 | if (mapUuid !== null && mapUuid.length > 0 && !this.mapStylingService.inPrintEditorMode) { 87 | this.mapStylingService.addBasemap(mapUuid, true, true); 88 | } else if (this.mapStylingService.inPrintEditorMode) { 89 | this.mapStylingService.addBasemap('printEditor', true, false, this.route.snapshot.queryParamMap.get('style')); 90 | } 91 | } 92 | 93 | ngOnDestroy() { 94 | this.activeBasemapChangedSubscription && this.activeBasemapChangedSubscription.unsubscribe(); 95 | this.activeStylingChangedSubscription && this.activeStylingChangedSubscription.unsubscribe(); 96 | } 97 | /** 98 | * Read metadata of groups and layers 99 | */ 100 | parseMetadata() { 101 | this.hasGuiLayers = false; 102 | this.showGroupConfiguration = false; 103 | this.showGuiLayerConfiguration = false; 104 | this.mapFunctionService.guiLayerState = false; 105 | this.mapFunctionService.groupLayerState = false; 106 | // Read metadata of groups and GUI layers 107 | for (const layer of this.activeStyling.layers) { 108 | if (layer.metadata && layer.metadata['map-editor:layer']) { 109 | this.hasGuiLayers = true; 110 | this.mapFunctionService.guiLayerState = true; 111 | } 112 | if (layer.metadata && layer.metadata['map-editor:group'] && layer.metadata['map-editor:detail-level']) { 113 | this.showGroupConfiguration = true; 114 | this.mapFunctionService.groupLayerState = true; 115 | } 116 | // Stop iterating when groups and GUI layers found 117 | if (this.hasGuiLayers === true && this.showGroupConfiguration === true) { 118 | break; 119 | } 120 | } 121 | this.mapFunctionService.metadataChanged.emit(); 122 | } 123 | 124 | /** 125 | * Toolbar button clicked 126 | * @param tool Activated MapTool 127 | */ 128 | onToolbarButtonClick(tool: MapTool) { 129 | this.router.navigate(['/map', tool.link]); 130 | this.showToolOverlay = true; 131 | } 132 | 133 | /** 134 | * Change height of tool overlay 135 | * @param height New height for tool overlay 136 | */ 137 | onResizeToolOverlay(height: number) { 138 | this.toolOverlayHeight = height; 139 | } 140 | 141 | /** 142 | * Close tool overlay 143 | */ 144 | onCloseToolOverlay() { 145 | this.showToolOverlay = false; 146 | this.headerService.changeTitle(this.headerTitle); 147 | this.router.navigate(['/map']); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/app/map/maplibre-gl/maplibre-gl.component.scss: -------------------------------------------------------------------------------- 1 | .map { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | 6 | .mapboxgl-ctrl-group > button { 7 | width: 40px; 8 | height: 40px; 9 | 10 | .material-icons { 11 | font-size: 24px; 12 | } 13 | } 14 | 15 | .mapboxgl-ctrl-icon.mapboxgl-ctrl-compass > .mapboxgl-ctrl-compass-arrow { 16 | width: 24px; 17 | height: 24px; 18 | } 19 | 20 | .info-popup { 21 | max-height: 400px; 22 | 23 | .mapboxgl-popup-content { 24 | overflow: auto; 25 | padding-top: 30px; 26 | 27 | .mapboxgl-popup-close-button { 28 | font-size: 40px; 29 | position: fixed; 30 | margin: 10px 20px 0; 31 | background-color: #FFF; 32 | padding: 0; 33 | } 34 | 35 | .info-table { 36 | font-size: 15px; 37 | border-bottom: 1px solid grey; 38 | padding: 10px 0; 39 | } 40 | } 41 | } 42 | 43 | .searchInput { 44 | height: 50px; 45 | border: none; 46 | margin: 0; padding: 6px; 47 | font-size: 18px; 48 | 49 | &.hidden { 50 | display: none; 51 | } 52 | 53 | &:focus { 54 | outline: none; 55 | } 56 | } 57 | 58 | .resultArea { 59 | background-color: #FFF; 60 | width: 100%; 61 | 62 | &.hidden { 63 | display: none; 64 | } 65 | 66 | .result-row { 67 | height: 50px; 68 | font-size: 16px; 69 | max-width: 80vw; 70 | border-top: 1px solid lightgrey; 71 | padding: 5px 5px; 72 | 73 | &:hover { 74 | background-color: #e9efff; 75 | } 76 | 77 | .result-row-main { 78 | font-weight: bold; 79 | } 80 | } 81 | } 82 | 83 | .zoom-control { 84 | margin: 10px 5px; 85 | opacity: 0.8; 86 | font-size: 20px; 87 | text-align: right; 88 | 89 | @media (max-width: 576px) { 90 | display: none; 91 | } 92 | 93 | span { 94 | background-color: white; 95 | padding: 5px; 96 | } 97 | } 98 | 99 | .mapboxgl-ctrl-scale { 100 | @media (max-width: 576px) { 101 | display: none; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/app/map/maplibre-gl/maplibre-gl.pitch.control.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Pitch control for MapLibre GL 3 | * The button toggles the map view between tilted and normal map view 4 | */ 5 | export default class MaplibreGlPitchControl { 6 | private map; 7 | private btn; 8 | private container; 9 | 10 | constructor() { } 11 | 12 | onAdd(map) { 13 | this.map = map; 14 | this.btn = document.createElement('button'); 15 | this.btn.className = 'mapboxgl-ctrl-icon icons pitch-control'; 16 | this.btn.type = 'button'; 17 | this.btn.title = 'Neigung einstellen'; 18 | this.btn.onclick = () => { 19 | const pitch = (map.getPitch() > 0) ? 0 : 60; 20 | map.easeTo({pitch: pitch}); 21 | }; 22 | 23 | const icon = document.createElement('mat-icon'); 24 | icon.className = 'material-icons'; 25 | const iconName = document.createTextNode('swap_vert'); 26 | icon.append(iconName); 27 | this.btn.append(icon); 28 | 29 | this.container = document.createElement('div'); 30 | this.container.className = 'mapboxgl-ctrl mapboxgl-ctrl-group'; 31 | this.container.append(this.btn); 32 | 33 | return this.container; 34 | } 35 | 36 | onRemove() { 37 | this.container.parentNode.removeChild(this.container); 38 | this.map = undefined; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/app/map/maplibre-gl/maplibre-gl.search.control.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Search control for MapLibre GL 3 | */ 4 | export default class MaplibreGlSearchControl { 5 | private map; 6 | private btn; 7 | private input; 8 | private resultArea; 9 | private container; 10 | private mapServiceUrl; 11 | 12 | /** 13 | * Constructor 14 | * @param mapServiceUrl URL of VT Map Service 15 | */ 16 | constructor(mapServiceUrl: string) { 17 | this.mapServiceUrl = mapServiceUrl; 18 | } 19 | 20 | onAdd(map) { 21 | // Add the mapServiceUrl to the map object to access it via event functions 22 | map.mapServiceUrl = this.mapServiceUrl; 23 | 24 | this.map = map; 25 | this.input = document.createElement('input'); 26 | this.input.type = 'text'; 27 | this.input.placeholder = 'Suche'; 28 | this.input.id = 'searchInput'; 29 | this.input.className = 'searchInput hidden'; 30 | this.input.oninput = (event: any) => { 31 | const term = event.target.value; 32 | if (term.length > 2) { 33 | document.getElementById('resultArea').classList.remove('hidden'); 34 | 35 | const xhttp = new XMLHttpRequest(); 36 | xhttp.onreadystatechange = function() { 37 | if (this.readyState === 4 && this.status === 200) { 38 | const response = JSON.parse(this.responseText); 39 | 40 | document.getElementById('resultArea').innerHTML = ''; 41 | 42 | for (const respEntry of response.suggestions) { 43 | const result = document.createElement('div'); 44 | result.className = 'result-row'; 45 | let mainText = ''; 46 | let subText = ''; 47 | 48 | if (respEntry.suggestion.search(/,/) > -1) { 49 | mainText = respEntry.suggestion.split(',')[0].trim(); 50 | subText = respEntry.suggestion.split(',')[1].trim(); 51 | } else { 52 | mainText = respEntry.suggestion; 53 | } 54 | 55 | const mainTextDiv = document.createElement('div'); 56 | mainTextDiv.className = 'result-row-main'; 57 | mainTextDiv.textContent = mainText; 58 | const subTextDiv = document.createElement('div'); 59 | subTextDiv.className = 'result-row-sub'; 60 | subTextDiv.textContent = subText; 61 | result.append(mainTextDiv, subTextDiv); 62 | result.onclick = () => { 63 | (document.getElementById('searchInput') as HTMLInputElement).value = respEntry.suggestion; 64 | const termResult = respEntry.suggestion; 65 | const xhttpResult = new XMLHttpRequest(); 66 | 67 | xhttpResult.onreadystatechange = function() { 68 | if (this.readyState === 4 && this.status === 200) { 69 | { 70 | const responseResult = JSON.parse(this.responseText); 71 | map.fitBounds([ 72 | [responseResult.features[0].bbox[0], responseResult.features[0].bbox[1]], 73 | [responseResult.features[0].bbox[2], responseResult.features[0].bbox[3]]] 74 | ); 75 | } 76 | } 77 | }; 78 | 79 | if (termResult.length > 0) { 80 | xhttpResult.open('GET', map.mapServiceUrl + '/search?term=' + termResult, true); 81 | xhttpResult.send(); 82 | } 83 | 84 | document.getElementById('resultArea').innerHTML = ''; 85 | document.getElementById('resultArea').classList.add('hidden'); 86 | }; 87 | document.getElementById('resultArea').appendChild(result); 88 | } 89 | } 90 | }; 91 | 92 | xhttp.open('GET', map.mapServiceUrl + '/suggest?term=' + term, true); 93 | xhttp.send(); 94 | } else { 95 | document.getElementById('resultArea').innerHTML = ''; 96 | document.getElementById('resultArea').classList.add('hidden'); 97 | } 98 | }; 99 | 100 | this.resultArea = document.createElement('div'); 101 | this.resultArea.id = 'resultArea'; 102 | this.resultArea.className = 'resultArea'; 103 | 104 | this.btn = document.createElement('button'); 105 | this.btn.className = 'mapboxgl-ctrl-icon icons search-control'; 106 | this.btn.type = 'button'; 107 | this.btn.title = 'Adresssuche'; 108 | this.btn.style.display = 'inline'; 109 | this.btn.onclick = () => { 110 | document.getElementById('resultArea').innerHTML = ''; 111 | (document.getElementById('searchInput') as HTMLInputElement).value = ''; 112 | if (this.input.classList.contains('hidden')) { 113 | this.input.classList.remove('hidden'); 114 | } else { 115 | this.input.classList.add('hidden'); 116 | } 117 | }; 118 | 119 | const icon = document.createElement('mat-icon'); 120 | icon.className = 'material-icons'; 121 | const iconName = document.createTextNode('search'); 122 | icon.append(iconName); 123 | this.btn.append(icon); 124 | 125 | this.container = document.createElement('div'); 126 | this.container.className = 'mapboxgl-ctrl mapboxgl-ctrl-group'; 127 | this.container.append(this.input); 128 | this.container.append(this.btn); 129 | this.container.append(this.resultArea); 130 | 131 | return this.container; 132 | } 133 | 134 | onRemove() { 135 | this.container.parentNode.removeChild(this.container); 136 | this.map = undefined; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/app/map/maplibre-gl/maplibre-gl.show-zoom.control.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Control for showing the zoom level in MapLibre GL 3 | */ 4 | export default class MaplibreGlShowZoomControl { 5 | private map; 6 | private container; 7 | private zoomControl; 8 | private zoomControlWrapper; 9 | private zoomText; 10 | 11 | constructor() { } 12 | 13 | onAdd(map) { 14 | this.map = map; 15 | this.zoomControl = document.createElement('div'); 16 | this.zoomControl.id = 'zoom-control'; 17 | this.zoomControl.className = 'zoom-control'; 18 | this.zoomText = document.createTextNode((Math.round(this.map.getZoom() * 100) / 100).toString()); 19 | this.zoomControlWrapper = document.createElement('span'); 20 | this.zoomControlWrapper.append(this.zoomText); 21 | this.zoomControl.append(this.zoomControlWrapper); 22 | 23 | this.container = document.createElement('div'); 24 | this.container.append(this.zoomControl); 25 | 26 | return this.container; 27 | } 28 | 29 | onRemove() { 30 | this.container.parentNode.removeChild(this.container); 31 | this.map = undefined; 32 | } 33 | 34 | /** 35 | * Change zoom level text 36 | * @param zoomlevel Zoomlevel 37 | */ 38 | changeText(zoomlevel: number) { 39 | this.zoomControlWrapper.removeChild(this.zoomText); 40 | this.zoomText = document.createTextNode((Math.round(this.map.getZoom() * 100) / 100).toString()); 41 | this.zoomControlWrapper.append(this.zoomText); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/app/map/tools/map-tool.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * tool for map toolbar 3 | */ 4 | export class MapTool { 5 | constructor( 6 | public icon: string, 7 | public link: string 8 | ) {} 9 | } 10 | -------------------------------------------------------------------------------- /src/app/map/tools/tool-basemap/tool-basemap.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | {{basemap.name}} 11 | 12 | 13 | 14 | cloud_download 15 | 16 | 17 | Helligkeit 18 | 19 | expand_less 20 | 21 | 22 | expand_more 23 | 24 | 25 | 26 | Sättigung 27 | 28 | expand_less 29 | 30 | 31 | expand_more 32 | 33 | 34 | 35 | 36 | 37 | 38 | Basiskarte laden 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | Abbrechen 49 | Laden 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/app/map/tools/tool-basemap/tool-basemap.component.scss: -------------------------------------------------------------------------------- 1 | .basemap-card { 2 | max-width: 100px; 3 | width: 100px; 4 | margin: 5px; 5 | //height: 100px; 6 | 7 | .basemap-img { 8 | width: 100px; 9 | max-width: 100px; 10 | } 11 | 12 | .mat-card-content { 13 | text-align: center; 14 | } 15 | 16 | &.basemap-load { 17 | mat-icon { 18 | width: 70px; 19 | height: 70px; 20 | font-size: 70px; 21 | margin-top: 20px; 22 | } 23 | } 24 | } 25 | 26 | .col-config { 27 | max-width: 100px; 28 | margin: 5px; 29 | 30 | .config-title { 31 | margin: 5px 0; 32 | } 33 | 34 | button { 35 | height: 50px; 36 | margin: 5px 0; 37 | } 38 | } 39 | 40 | .load-basemap-form { 41 | mat-form-field { 42 | width: 100%; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/app/map/tools/tool-basemap/tool-basemap.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Basemap } from 'src/app/shared/basemap'; 3 | import { HeaderService } from 'src/app/header/header.service'; 4 | import { MapStylingService } from 'src/app/map/map-styling.service'; 5 | 6 | 7 | /** 8 | * Component for selecting a basemap 9 | */ 10 | @Component({ 11 | selector: 'app-tool-basemap', 12 | templateUrl: './tool-basemap.component.html', 13 | styleUrls: ['./tool-basemap.component.scss'] 14 | }) 15 | export class ToolBasemapComponent implements OnInit { 16 | basemaps: Basemap[]; 17 | activeBasemap: Basemap; 18 | showLoadBasemap: boolean; 19 | 20 | constructor(private headerService: HeaderService, private mapStylingService: MapStylingService) {} 21 | 22 | ngOnInit() { 23 | this.headerService.changeTitle('Basiskarte'); 24 | this.basemaps = this.mapStylingService.basemaps; 25 | this.activeBasemap = this.mapStylingService.activeBasemap; 26 | this.showLoadBasemap = false; 27 | } 28 | 29 | /** 30 | * Set selected basemap to active basemap 31 | * @param basemap Selected basemap 32 | */ 33 | onBasemapSelected(basemap) { 34 | this.activeBasemap = basemap; 35 | this.mapStylingService.changeActiveBasemap(basemap, false); 36 | } 37 | 38 | /** 39 | * Show user interface for loading a stored styling as basemap 40 | */ 41 | toggleShowLoadBasemap() { 42 | this.showLoadBasemap = !this.showLoadBasemap; 43 | } 44 | 45 | /** 46 | * Loading a stored styling as basemap 47 | * @param basemapId basemap ID 48 | */ 49 | onLoadBasemapStart(basemapId: string) { 50 | let uuid = basemapId.toLowerCase(); 51 | // Check entered URL 52 | if (basemapId.search(/\?id\=/) > -1) { 53 | // Editor URL 54 | uuid = basemapId.substring(basemapId.lastIndexOf('?id=') + 4); 55 | } else if (basemapId.search(/\//) > -1) { 56 | // Style or Aplication URL 57 | uuid = basemapId.substring(basemapId.lastIndexOf('/') + 1); 58 | } 59 | // Check UUID format 60 | if (uuid.search(/\b[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}\b$/) === 0) { 61 | // Load basemap 62 | const newBasemap: Basemap = this.mapStylingService.addBasemap(uuid, true, false); 63 | this.activeBasemap = newBasemap; 64 | this.showLoadBasemap = false; 65 | } 66 | } 67 | 68 | /** 69 | * Increase map lightness 70 | */ 71 | onLightnessUp() { 72 | this.mapStylingService.changeHSL(0, 10); 73 | } 74 | 75 | /** 76 | * Decrease map lightness 77 | */ 78 | onLightnessDown() { 79 | this.mapStylingService.changeHSL(0, -10); 80 | } 81 | 82 | /** 83 | * Increase map saturation 84 | */ 85 | onSaturationUp() { 86 | this.mapStylingService.changeHSL(10, 0); 87 | } 88 | 89 | /** 90 | * Decrease map saturation 91 | */ 92 | onSaturationDown() { 93 | this.mapStylingService.changeHSL(-10, 0); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/app/map/tools/tool-edit/group-configuration/group-configuration.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{group}} 4 | 5 | 11 | 12 | 13 | Mehr Details 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/app/map/tools/tool-edit/group-configuration/group-configuration.component.scss: -------------------------------------------------------------------------------- 1 | mat-slider { 2 | width: 100%; 3 | } 4 | 5 | .group-slider { 6 | font-size: 24px; 7 | max-width: 400px; 8 | margin: auto; 9 | } 10 | 11 | a { 12 | font-size: 22px; 13 | } 14 | 15 | .btn-detail { 16 | margin-bottom: 20px; 17 | } 18 | -------------------------------------------------------------------------------- /src/app/map/tools/tool-edit/group-configuration/group-configuration.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | 3 | import { GroupConfigurationComponent } from './group-configuration.component'; 4 | import {MapStylingService} from '../../../map-styling.service'; 5 | import {MapFunctionService} from '../../../map-function.service'; 6 | import {RouterTestingModule} from '@angular/router/testing'; 7 | import {MaterialDesignModule} from '../../../../material-design/material-design.module'; 8 | import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; 9 | import {HttpClientTestingModule} from '@angular/common/http/testing'; 10 | 11 | describe('GroupConfigurationComponent', () => { 12 | let component: GroupConfigurationComponent; 13 | let fixture: ComponentFixture; 14 | 15 | beforeEach(waitForAsync(() => { 16 | TestBed.configureTestingModule({ 17 | declarations: [GroupConfigurationComponent], 18 | imports: [ 19 | RouterTestingModule, 20 | MaterialDesignModule, 21 | BrowserAnimationsModule, 22 | HttpClientTestingModule, 23 | ], 24 | providers: [ 25 | {provide: MapStylingService, useClass: MapStylingServiceStub}, 26 | {provide: MapFunctionService, useClass: MapFunctionServiceStub}, 27 | 28 | ] 29 | }) 30 | .compileComponents(); 31 | })); 32 | 33 | beforeEach(() => { 34 | fixture = TestBed.createComponent(GroupConfigurationComponent); 35 | component = fixture.componentInstance; 36 | fixture.detectChanges(); 37 | }); 38 | 39 | it('should create', () => { 40 | expect(component).toBeTruthy(); 41 | }); 42 | }); 43 | class MapStylingServiceStub{ 44 | activeStyling = {layers: [{}]}; 45 | } 46 | class MapFunctionServiceStub {} 47 | -------------------------------------------------------------------------------- /src/app/map/tools/tool-edit/group-configuration/group-configuration.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; 2 | import { MapStylingService } from 'src/app/map/map-styling.service'; 3 | import {MapFunctionService} from '../../../map-function.service'; 4 | import {Router} from '@angular/router'; 5 | 6 | /** 7 | * Component for layer group configurations 8 | */ 9 | @Component({ 10 | selector: 'app-group-configuration', 11 | templateUrl: './group-configuration.component.html', 12 | styleUrls: ['./group-configuration.component.scss'] 13 | }) 14 | export class GroupConfigurationComponent implements OnInit { 15 | @Input() hasGuiLayers: boolean; 16 | @Output() toggleGroupConfiguration = new EventEmitter(); 17 | layerGroups: string[]; 18 | groupSettings: any; 19 | showGuiLayerConfiguration: boolean; 20 | activeStyling: any; 21 | 22 | constructor(private mapStylingService: MapStylingService, 23 | private mapFunctionService: MapFunctionService, 24 | private router: Router) { } 25 | 26 | ngOnInit() { 27 | this.layerGroups = []; 28 | for (const layer of this.mapStylingService.activeStyling.layers) { 29 | if (layer.metadata && layer.metadata['map-editor:group'] 30 | && layer.metadata['map-editor:detail-level'] && this.layerGroups.indexOf(layer.metadata['map-editor:group']) === -1) { 31 | this.layerGroups.push(layer.metadata['map-editor:group']); 32 | } 33 | } 34 | this.groupSettings = this.mapStylingService.groupSettings; 35 | this.hasGuiLayers = this.mapFunctionService.guiLayerState; 36 | } 37 | 38 | /** 39 | * Detail slider of a layer group was changed 40 | * @param groupName Group name 41 | * @param event Event 42 | */ 43 | onGroupLevelChanged(groupName: string, event: any) { 44 | this.mapStylingService.changeGroupDetailLevel(groupName, event.value); 45 | } 46 | 47 | /** 48 | * Change GUI from group configuration to gui-layer configuration 49 | */ 50 | showLayerConfiguration() { 51 | this.router.navigate(['/map', 'edit', 'gui-layer']); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/app/map/tools/tool-edit/gui-layer/gui-layer-configuration.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{group.name}} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {{guiLayer.name}} 15 | 16 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/app/map/tools/tool-edit/gui-layer/gui-layer-configuration.component.scss: -------------------------------------------------------------------------------- 1 | app-gui-layer-configuration { 2 | .mat-expansion-panel-header { 3 | font-size: 22px; 4 | } 5 | 6 | .gui-layer-config { 7 | .gui-layer-header { 8 | margin-bottom: 20px; 9 | margin-top: 20px; 10 | } 11 | 12 | .gui-layer-name { 13 | font-size: 24px; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/app/map/tools/tool-edit/gui-layer/gui-layer-configuration.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input, ViewEncapsulation } from '@angular/core'; 2 | import { MapStylingService } from 'src/app/map/map-styling.service'; 3 | import { AppConfigService } from 'src/app/app-config.service'; 4 | 5 | /** 6 | * Component to configure GUI layers 7 | */ 8 | @Component({ 9 | selector: 'app-gui-layer-configuration', 10 | templateUrl: './gui-layer-configuration.component.html', 11 | styleUrls: ['./gui-layer-configuration.component.scss'], 12 | encapsulation: ViewEncapsulation.None 13 | }) 14 | export class GuiLayerConfigurationComponent implements OnInit { 15 | groups: Array<{ 16 | name: string, 17 | guiLayers: Array<{ 18 | name: string, 19 | visible: boolean, 20 | elements: Array<{ 21 | name: string, 22 | layer: any 23 | }> 24 | }> 25 | }>; 26 | // Sortet list of GUI layers 27 | keyNames: Array; 28 | 29 | constructor(private mapStylingService: MapStylingService) { } 30 | 31 | ngOnInit() { 32 | this.parseMetadata(); 33 | } 34 | 35 | /** 36 | * Read metadata for groups and gui layers from styling 37 | */ 38 | parseMetadata() { 39 | this.groups = []; 40 | // Stores combinations of names of the groups, GUI layers and layer elements that are already added to groups array 41 | this.keyNames = []; 42 | 43 | for (const layer of this.mapStylingService.activeStyling.layers) { 44 | if (layer.metadata) { 45 | let groupIdx = -1; 46 | let layerIdx = -1; 47 | 48 | // Add new group 49 | if (layer.metadata['map-editor:group'] && this.keyNames.indexOf(layer.metadata['map-editor:group']) === -1) { 50 | this.keyNames.push(layer.metadata['map-editor:group']); 51 | groupIdx = this.groups.push({ 52 | name: layer.metadata['map-editor:group'], 53 | guiLayers: [] 54 | }); 55 | groupIdx--; 56 | } 57 | 58 | // Add new layer 59 | if (layer.metadata['map-editor:layer'] 60 | && this.keyNames.indexOf(layer.metadata['map-editor:group'] + layer.metadata['map-editor:layer']) === -1) { 61 | 62 | this.keyNames.push(layer.metadata['map-editor:group'] + layer.metadata['map-editor:layer']); 63 | let guiLayerVisible = true; 64 | const guiLayerSettings = this.mapStylingService.guiLayerSettings[layer.metadata['map-editor:layer']]; 65 | if (guiLayerSettings !== undefined) { 66 | guiLayerVisible = guiLayerSettings.visible; 67 | } 68 | 69 | if (groupIdx === -1) { 70 | groupIdx = this.groups.findIndex(group => group.name === layer.metadata['map-editor:group']); 71 | } 72 | layerIdx = this.groups[groupIdx].guiLayers.push({ 73 | name: layer.metadata['map-editor:layer'], 74 | visible: guiLayerVisible, 75 | elements: [] 76 | }); 77 | 78 | layerIdx--; 79 | } 80 | 81 | // Add new layer element 82 | if (layer.metadata['map-editor:layer-element'] 83 | && this.keyNames.indexOf(layer.metadata['map-editor:group'] + layer.metadata['map-editor:layer'] + layer.metadata['map-editor:layer-element']) === -1) { 84 | 85 | this.keyNames.push(layer.metadata['map-editor:group'] + layer.metadata['map-editor:layer'] + layer.metadata['map-editor:layer-element']); 86 | 87 | if (groupIdx === -1) { 88 | groupIdx = this.groups.findIndex(group => group.name === layer.metadata['map-editor:group']); 89 | } 90 | if (layerIdx === -1) {layerIdx = this.groups[groupIdx].guiLayers.findIndex(guiLayer => 91 | guiLayer.name === layer.metadata['map-editor:layer']); 92 | } 93 | 94 | // Add layer element with selection of layer properties 95 | this.groups[groupIdx].guiLayers[layerIdx].elements.push( 96 | { 97 | name: layer.metadata['map-editor:layer-element'], 98 | layer: { 99 | type: layer['type'], 100 | paint: layer['paint'] 101 | } 102 | } 103 | ); 104 | } 105 | } 106 | } 107 | 108 | // Sort arrays 109 | this.sortGroupsAndLayers(); 110 | } 111 | 112 | /** 113 | * Change GUI layer visibility 114 | * @param layerName GUI-Layer Name 115 | * @param event Event 116 | */ 117 | onVisibilityChanged(guiLayerName: string, event: any) { 118 | this.mapStylingService.changeGuiLayerVisibility(guiLayerName, event.checked); 119 | } 120 | 121 | /** 122 | * Sort arrays of groups and GUI layers by their names 123 | */ 124 | sortGroupsAndLayers() { 125 | this.groups.sort(this.compareNames); 126 | 127 | for (const group of this.groups) { 128 | group.guiLayers.sort(this.compareNames); 129 | } 130 | } 131 | 132 | /** 133 | * Compare two object by their attribute "name" 134 | * @param obj1 Object 1 135 | * @param obj2 Object 2 136 | */ 137 | compareNames(obj1, obj2) { 138 | const objName1 = obj1.name.toLowerCase(); 139 | const objName2 = obj2.name.toLowerCase(); 140 | 141 | let compare = 0; 142 | if (objName1 > objName2) { 143 | compare = 1; 144 | } else if (objName1 < objName2) { 145 | compare = -1; 146 | } 147 | 148 | return compare; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/app/map/tools/tool-edit/gui-layer/gui-layer-element.component.html: -------------------------------------------------------------------------------- 1 | 2 | 16 | {{elementName}} 17 | -------------------------------------------------------------------------------- /src/app/map/tools/tool-edit/gui-layer/gui-layer-element.component.scss: -------------------------------------------------------------------------------- 1 | app-gui-layer-element { 2 | font-size: 22px; 3 | 4 | .element-label { 5 | position: relative; 6 | bottom: 15px; 7 | } 8 | 9 | .color-btn { 10 | height: 50px; 11 | width: 50px; 12 | border-radius: 25px; 13 | display: inline-block; 14 | border: 1px solid grey; 15 | margin-left: 20px; 16 | margin-right: 20px; 17 | } 18 | } 19 | 20 | .color-picker{ 21 | left: 10px !important; 22 | top: calc(100% - 350px) !important; 23 | } 24 | -------------------------------------------------------------------------------- /src/app/map/tools/tool-edit/gui-layer/gui-layer-element.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | 3 | import { GuiLayerElementComponent } from './gui-layer-element.component'; 4 | import {MapStylingService} from '../../../map-styling.service'; 5 | import {ColorPickerModule} from 'ngx-color-picker'; 6 | 7 | describe('GuiLayerElementComponent', () => { 8 | let component: GuiLayerElementComponent; 9 | let fixture: ComponentFixture; 10 | 11 | 12 | beforeEach(waitForAsync(() => { 13 | TestBed.configureTestingModule({ 14 | declarations: [ 15 | GuiLayerElementComponent, 16 | ], 17 | imports: [ 18 | ColorPickerModule 19 | ], 20 | providers: [{provide: MapStylingService, useClass: MapStylingServiceStub}] 21 | }) 22 | .compileComponents(); 23 | })); 24 | 25 | beforeEach(() => { 26 | fixture = TestBed.createComponent(GuiLayerElementComponent); 27 | component = fixture.componentInstance; 28 | const layer = {type: {}}; 29 | component.layer = layer; 30 | fixture.detectChanges(); 31 | 32 | }); 33 | 34 | it('should create', () => { 35 | const layer = {type: {}}; 36 | component.layer = layer; 37 | expect(component).toBeTruthy(); 38 | }); 39 | }); 40 | class MapStylingServiceStub{ 41 | activeStyling = {layers: [{}]}; 42 | } 43 | -------------------------------------------------------------------------------- /src/app/map/tools/tool-edit/gui-layer/gui-layer-element.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit, ChangeDetectorRef, Input, ViewEncapsulation} from '@angular/core'; 2 | import { MapStylingService } from 'src/app/map/map-styling.service'; 3 | 4 | /** 5 | * Element of a GUI layer (e.g. fill, outline) 6 | */ 7 | @Component({ 8 | selector: 'app-gui-layer-element', 9 | templateUrl: './gui-layer-element.component.html', 10 | styleUrls: ['./gui-layer-element.component.scss'], 11 | encapsulation: ViewEncapsulation.None, 12 | }) 13 | export class GuiLayerElementComponent implements OnInit{ 14 | @Input() guiLayerName: string; 15 | @Input() elementName: string; 16 | @Input() layer: any; 17 | color: string; 18 | showColorPicker: boolean; 19 | 20 | constructor(private mapStylingService: MapStylingService, private cdr: ChangeDetectorRef) { } 21 | ngOnInit() { 22 | this.showColorPicker = false; 23 | // Read paint attributes from styling 24 | const colorType = (this.layer.type === 'symbol') ? 'text' : this.layer.type; 25 | const colorAttribute = colorType + '-color'; 26 | const opacityAttribute = colorType + '-opacity'; 27 | if (this.layer.paint !== undefined) { 28 | // tslint:disable-next-line:max-line-length 29 | if (this.layer.paint[colorAttribute] !== undefined && this.layer.paint[opacityAttribute] !== undefined && typeof this.layer.paint[opacityAttribute] !== 'object') { 30 | // tslint:disable-next-line:max-line-length 31 | this.color = this.mapStylingService.changeColorToSupported(this.layer.paint[colorAttribute], this.layer.paint[opacityAttribute]); 32 | } else if (this.layer.paint[colorAttribute] !== undefined ) { 33 | this.color = this.mapStylingService.changeColorToSupported(this.layer.paint[colorAttribute]); 34 | } else { 35 | this.color = '#000000'; 36 | } 37 | } 38 | } 39 | 40 | /** 41 | * Change color 42 | * @param color Color (hex, rgb, rgba, hsl, hsla) 43 | */ 44 | onColorChanged(color: string) { 45 | this.color = color; 46 | this.mapStylingService.changeGuiLayerColor(this.guiLayerName, this.elementName, color); 47 | this.cdr.detectChanges(); 48 | } 49 | 50 | /** 51 | * Change visibility of colorPicker 52 | */ 53 | changeVisibility(){ 54 | this.showColorPicker = !this.showColorPicker; 55 | this.cdr.detectChanges(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/app/map/tools/tool-edit/layer/layer-configuration.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/map/tools/tool-edit/layer/layer-configuration.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Basisvisualisierung/vt-map-editor/20daf75105c50a236f4cd4664c7c28445131bfcc/src/app/map/tools/tool-edit/layer/layer-configuration.component.scss -------------------------------------------------------------------------------- /src/app/map/tools/tool-edit/layer/layer-configuration.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { LayerConfigurationComponent } from './layer-configuration.component'; 3 | import {MapStylingService} from '../../../map-styling.service'; 4 | import {MaterialDesignModule} from '../../../../material-design/material-design.module'; 5 | import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; 6 | import {LayerElementComponent} from './layer-element.component'; 7 | 8 | describe('LayerConfigurationComponent', () => { 9 | let component: LayerConfigurationComponent; 10 | let fixture: ComponentFixture; 11 | 12 | beforeEach(waitForAsync(() => { 13 | TestBed.configureTestingModule({ 14 | imports: [ 15 | MaterialDesignModule, 16 | BrowserAnimationsModule, 17 | ], 18 | declarations: [ 19 | LayerConfigurationComponent, 20 | LayerElementComponent 21 | ], 22 | providers: [ 23 | {provide: MapStylingService, useClass: MapStylingServiceStub} 24 | ] 25 | }) 26 | .compileComponents(); 27 | })); 28 | 29 | beforeEach(() => { 30 | fixture = TestBed.createComponent(LayerConfigurationComponent); 31 | component = fixture.componentInstance; 32 | fixture.detectChanges(); 33 | }); 34 | 35 | it('should create', () => { 36 | expect(component).toBeTruthy(); 37 | }); 38 | }); 39 | 40 | class MapStylingServiceStub{ 41 | activeStyling = {layers: [{}]}; 42 | } 43 | -------------------------------------------------------------------------------- /src/app/map/tools/tool-edit/layer/layer-configuration.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import {MapStylingService} from '../../../map-styling.service'; 3 | 4 | @Component({ 5 | selector: 'app-layer-configuration', 6 | templateUrl: './layer-configuration.component.html', 7 | styleUrls: ['./layer-configuration.component.scss'] 8 | }) 9 | export class LayerConfigurationComponent implements OnInit { 10 | activeStyling: any; 11 | constructor(private mapStylingService: MapStylingService) { } 12 | 13 | ngOnInit(): void { 14 | this.activeStyling = this.mapStylingService.activeStyling; 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/app/map/tools/tool-edit/layer/layer-element.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ layer.id }} 5 | 6 | 7 | 8 | 9 | 10 | 11 | Farbe: 12 | 13 | 29 | 30 | 31 | 32 | Deckkraft: 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/app/map/tools/tool-edit/layer/layer-element.component.scss: -------------------------------------------------------------------------------- 1 | $font-size: 22px; 2 | 3 | app-layer-element { 4 | .mat-expansion-panel-header { 5 | font-size: $font-size; 6 | } 7 | 8 | .mat-slider { 9 | width: 100%; 10 | max-width: 400px; 11 | padding: 8px 0; 12 | 13 | &.mat-slider-horizontal { 14 | .mat-slider-wrapper { 15 | left: 0; 16 | right: 0; 17 | } 18 | } 19 | } 20 | } 21 | 22 | .layer-tools { 23 | label { 24 | font-size: $font-size; 25 | display: block; 26 | margin: 20px 0 10px; 27 | } 28 | } 29 | 30 | .slider-opacity-layer { 31 | width: 180px; 32 | } 33 | 34 | .color-picker { 35 | left: 10px !important; 36 | top: calc(100% - 450px) !important; 37 | } 38 | 39 | .color-btn { 40 | height: 50px; 41 | width: 100px; 42 | display: inline-block; 43 | border: 1px solid grey; 44 | } 45 | -------------------------------------------------------------------------------- /src/app/map/tools/tool-edit/layer/layer-element.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input, ViewEncapsulation } from '@angular/core'; 2 | import { MapStylingService } from '../../../map-styling.service'; 3 | 4 | /** 5 | * Configuration tools for a layer 6 | */ 7 | @Component({ 8 | selector: 'app-layer-element', 9 | templateUrl: './layer-element.component.html', 10 | styleUrls: ['./layer-element.component.scss'], 11 | encapsulation: ViewEncapsulation.None 12 | }) 13 | export class LayerElementComponent implements OnInit { 14 | @Input() layer; 15 | layerVisible: boolean; 16 | color: string; 17 | opacity: number; 18 | showColorPicker: boolean; 19 | 20 | constructor(private mapStylingService: MapStylingService) { } 21 | 22 | ngOnInit() { 23 | this.layerVisible = true; 24 | this.opacity = 1; 25 | this.showColorPicker = false; 26 | 27 | // Read color and opacity from styling 28 | const colorAttribute = this.layer.type + '-color'; 29 | const opacityAttribute = this.layer.type + '-opacity'; 30 | if (this.layer.paint !== undefined) { 31 | if (this.layer.paint[colorAttribute] !== undefined) { 32 | this.color = this.layer.paint[colorAttribute]; 33 | } else { 34 | this.color = '#000000'; 35 | } 36 | 37 | if (this.layer.paint[opacityAttribute] !== undefined) { 38 | this.opacity = this.layer.paint[opacityAttribute]; 39 | } 40 | } 41 | 42 | // Apply visibility 43 | if (this.layer.layout !== undefined && this.layer.layout.visibility !== undefined) { 44 | this.layerVisible = this.layer.layout.visibility === 'none' ? false : true; 45 | } 46 | } 47 | 48 | /** 49 | * Change visibility 50 | */ 51 | onVisibilityChanged() { 52 | this.layerVisible = !this.layerVisible; 53 | this.mapStylingService.changeLayerVisibility(this.layer.id, this.layerVisible); 54 | } 55 | 56 | /** 57 | * Show color picker 58 | */ 59 | onOpenColorPicker() { 60 | this.showColorPicker = true; 61 | } 62 | 63 | /** 64 | * Change fill color 65 | * @param color Color (hex, rgb, rgba, hsl, hsla) 66 | */ 67 | onColorChanged(color: string) { 68 | this.color = color; 69 | const colorType = (this.layer.type === 'symbol') ? 'text' : this.layer.type; 70 | this.mapStylingService.changeLayerAttribute(this.layer.id, 'paint.' + colorType + '-color', color); 71 | } 72 | 73 | /** 74 | * Change Opacity 75 | * @param event Slider event 76 | */ 77 | onOpacityChanged(event: any) { 78 | this.opacity = event.value; 79 | const attributeName = 'paint.' + this.layer.type + '-opacity'; 80 | this.mapStylingService.changeLayerAttribute(this.layer.id, attributeName, this.opacity); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/app/map/tools/tool-edit/tool-edit.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/app/map/tools/tool-edit/tool-edit.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Basisvisualisierung/vt-map-editor/20daf75105c50a236f4cd4664c7c28445131bfcc/src/app/map/tools/tool-edit/tool-edit.component.scss -------------------------------------------------------------------------------- /src/app/map/tools/tool-edit/tool-edit.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnDestroy, OnInit} from '@angular/core'; 2 | import { HeaderService } from 'src/app/header/header.service'; 3 | import { MapStylingService } from '../../map-styling.service'; 4 | import {MapFunctionService} from '../../map-function.service'; 5 | import {ActivatedRoute, Router} from '@angular/router'; 6 | /** 7 | * Tool for map editing 8 | */ 9 | @Component({ 10 | selector: 'app-tool-edit', 11 | templateUrl: './tool-edit.component.html', 12 | styleUrls: ['./tool-edit.component.scss'] 13 | }) 14 | export class ToolEditComponent implements OnInit, OnDestroy{ 15 | metadataChangedSubscription: any; 16 | activeStyling: any; 17 | deepLink: boolean; 18 | 19 | 20 | constructor(private router: Router, 21 | private headerService: HeaderService, 22 | private mapStylingService: MapStylingService, 23 | private mapFunctionService: MapFunctionService, 24 | public route: ActivatedRoute) { } 25 | 26 | ngOnInit() { 27 | this.headerService.changeTitle('Karte anpassen'); 28 | // listen when active styling is loaded if deep link is used 29 | this.mapStylingService.activeStylingChanged.subscribe( () => { 30 | this.activeStyling = this.mapStylingService.activeStyling; 31 | }); 32 | this.activeStyling = this.mapStylingService.activeStyling; 33 | // Check for deep link 34 | if (this.route.children.length !== 0) { 35 | this.deepLink = true; 36 | } 37 | // listen if metadata are loaded 38 | this.metadataChangedSubscription = this.mapFunctionService.metadataChanged.subscribe(() => { 39 | this.editSection(); 40 | }); 41 | this.editSection(); 42 | } 43 | 44 | ngOnDestroy() { 45 | this.metadataChangedSubscription.unsubscribe(); 46 | } 47 | 48 | /** 49 | * redirect to edit section depending on layer present on active styling 50 | */ 51 | editSection() { 52 | // Show GUI layers when only GUI layers and no groups are defined 53 | if (!this.mapFunctionService.groupLayerState && this.mapFunctionService.guiLayerState && !this.deepLink) { 54 | this.router.navigate(['/map', 'edit', 'gui-layer'], { replaceUrl: true }); 55 | } 56 | // Show layer when no GUI layers and no groups are defined 57 | else if (!this.mapFunctionService.groupLayerState && !this.mapFunctionService.guiLayerState && !this.deepLink) { 58 | this.router.navigate(['/map', 'edit', 'layer'], { replaceUrl: true }); 59 | } 60 | // Show group layer if this is not a deep link 61 | else if (!this.deepLink){ 62 | this.router.navigate(['/map', 'edit', 'group-layer'], { replaceUrl: true }); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/app/map/tools/tool-functions/tool-functions.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Steuerungselemente 4 | 5 | 6 | 7 | 8 | 9 | 10 | Info-Abfrage 11 | 12 | 13 | 14 | 15 | 16 | 17 | Suche 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/app/map/tools/tool-functions/tool-functions.component.scss: -------------------------------------------------------------------------------- 1 | .function-wrapper { 2 | font-size: 24px; 3 | 4 | .row { 5 | padding: 10px 0; 6 | max-width: 400px; 7 | margin: auto; 8 | .col-toggle { 9 | text-align: right; 10 | } 11 | 12 | .more { 13 | font-size: 34px; 14 | color: grey; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/map/tools/tool-functions/tool-functions.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | 3 | import { ToolFunctionsComponent } from './tool-functions.component'; 4 | import {HeaderService} from '../../../header/header.service'; 5 | import {MapFunctionService} from '../../map-function.service'; 6 | import {RouterTestingModule} from '@angular/router/testing'; 7 | import {MaterialDesignModule} from '../../../material-design/material-design.module'; 8 | import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; 9 | import {HttpClientTestingModule} from '@angular/common/http/testing'; 10 | 11 | describe('ToolFunctionsComponent', () => { 12 | let component: ToolFunctionsComponent; 13 | let fixture: ComponentFixture; 14 | 15 | beforeEach(waitForAsync(() => { 16 | TestBed.configureTestingModule({ 17 | imports: [ 18 | RouterTestingModule, 19 | MaterialDesignModule, 20 | BrowserAnimationsModule, 21 | HttpClientTestingModule 22 | ], 23 | declarations: [ToolFunctionsComponent], 24 | providers: [ 25 | {provide: HeaderService, useClass: HeaderServiceStub}, 26 | {provide: MapFunctionService, useClass: MapFunctionServiceStub} 27 | ] 28 | }) 29 | .compileComponents(); 30 | })); 31 | 32 | beforeEach(() => { 33 | fixture = TestBed.createComponent(ToolFunctionsComponent); 34 | component = fixture.componentInstance; 35 | fixture.detectChanges(); 36 | }); 37 | 38 | it('should create', () => { 39 | expect(component).toBeTruthy(); 40 | }); 41 | }); 42 | 43 | class HeaderServiceStub{ 44 | changeTitle(title: string) {} 45 | } 46 | 47 | class MapFunctionServiceStub{ 48 | mapFunctions = { 49 | navigation: { 50 | show: true, 51 | enabled: true 52 | }, 53 | info: { 54 | show: true, 55 | enabled: true 56 | }, 57 | search: { 58 | show: true, 59 | enabled: true 60 | } 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /src/app/map/tools/tool-functions/tool-functions.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { HeaderService } from 'src/app/header/header.service'; 3 | import { MapFunctionService } from '../../map-function.service'; 4 | import { MapFunctions } from 'src/app/shared/map-functions'; 5 | 6 | /** 7 | * Component for map functions 8 | */ 9 | @Component({ 10 | selector: 'app-tool-functions', 11 | templateUrl: './tool-functions.component.html', 12 | styleUrls: ['./tool-functions.component.scss'] 13 | }) 14 | export class ToolFunctionsComponent implements OnInit { 15 | mapFunctions: MapFunctions; 16 | 17 | constructor(private headerService: HeaderService, 18 | private mapFunctionService: MapFunctionService) { } 19 | 20 | ngOnInit() { 21 | this.headerService.changeTitle('Funktionen anpassen'); 22 | this.mapFunctions = this.mapFunctionService.mapFunctions; 23 | } 24 | 25 | /** 26 | * Toggle map function 27 | * @param functionName Name of map function 28 | * @param event Event 29 | */ 30 | onFunctionToggle(functionName: string, event: any) { 31 | this.mapFunctions[functionName].enable = event.checked; 32 | this.mapFunctionService.toggleMapFunction(functionName, event.checked); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/app/map/tools/tool-overlay/tool-overlay.component.html: -------------------------------------------------------------------------------- 1 | 8 | 9 | arrow_back 10 | 13 | drag_handle 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/app/map/tools/tool-overlay/tool-overlay.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../styles.scss'; 2 | 3 | .overlayContainer { 4 | width: 100%; 5 | background-color: #FFFFFF; 6 | position: absolute; 7 | bottom: 0; 8 | touch-action: manipulation; 9 | box-shadow: 0 0 10px grey; 10 | box-sizing: border-box; 11 | z-index: 3; 12 | overflow: auto; 13 | 14 | .overlay-header { 15 | text-align: center; 16 | position: fixed; 17 | background-color: #FFFFFF; 18 | z-index: 2; 19 | width: 100%; 20 | 21 | .overlay-close-btn { 22 | font-size: 30px; 23 | position: absolute; 24 | left: 10px; 25 | top: 5px; 26 | padding-top: 5px; 27 | width: 40px; 28 | height: 40px; 29 | cursor: pointer; 30 | } 31 | 32 | .overlayHandle { 33 | width: 100px; 34 | cursor: row-resize; 35 | margin: auto; 36 | 37 | mat-icon { 38 | font-size: 40px; 39 | } 40 | } 41 | } 42 | 43 | .overlayContent { 44 | max-height: calc(100vh - #{$header-height}); 45 | padding: 30px 10px; 46 | box-sizing: border-box; 47 | padding-top: 50px; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/app/map/tools/tool-overlay/tool-overlay.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { ResizableModule } from 'angular-resizable-element'; 3 | import { ToolOverlayComponent } from './tool-overlay.component'; 4 | import {RouterTestingModule} from '@angular/router/testing'; 5 | import {MaterialDesignModule} from '../../../material-design/material-design.module'; 6 | import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; 7 | import {HttpClientTestingModule} from '@angular/common/http/testing'; 8 | 9 | 10 | describe('ToolOverlayComponent', () => { 11 | let component: ToolOverlayComponent; 12 | let fixture: ComponentFixture; 13 | 14 | beforeEach(waitForAsync(() => { 15 | TestBed.configureTestingModule({ 16 | declarations: [ToolOverlayComponent], 17 | imports: [ 18 | RouterTestingModule, 19 | MaterialDesignModule, 20 | BrowserAnimationsModule, 21 | HttpClientTestingModule, 22 | ResizableModule 23 | ] 24 | }) 25 | .compileComponents(); 26 | })); 27 | 28 | beforeEach(() => { 29 | fixture = TestBed.createComponent(ToolOverlayComponent); 30 | component = fixture.componentInstance; 31 | fixture.detectChanges(); 32 | }); 33 | 34 | it('should create', () => { 35 | expect(component).toBeTruthy(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/app/map/tools/tool-overlay/tool-overlay.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild, ElementRef, EventEmitter, Output, Input, AfterViewInit } from '@angular/core'; 2 | import { ResizeEvent } from 'angular-resizable-element'; 3 | import { Router } from '@angular/router'; 4 | 5 | /** 6 | * Overlay view for map tools. 7 | */ 8 | @Component({ 9 | selector: 'app-tool-overlay', 10 | templateUrl: './tool-overlay.component.html', 11 | styleUrls: ['./tool-overlay.component.scss'] 12 | }) 13 | export class ToolOverlayComponent implements OnInit, AfterViewInit { 14 | @Input() overlayHeight: number; 15 | @Input() overlayMinHeight: number; 16 | 17 | @Output() overlayClosed = new EventEmitter(); 18 | @Output() overlayResized = new EventEmitter(); 19 | 20 | @ViewChild('toolOverlay') overlay: ElementRef; 21 | 22 | constructor(private router: Router) { } 23 | 24 | ngOnInit() {} 25 | 26 | ngAfterViewInit() { 27 | // Adjust height of the overlay after component is initialized. 28 | this.overlay.nativeElement.style.height = this.overlayHeight + 'px'; 29 | } 30 | 31 | /** 32 | * Overlay height changed 33 | * Height must not be less than overlayMinHeight. Height will be set to overlayMinHeight if larger. 34 | * The maximum height ist defined by max-height attribute in CSS 35 | * @param event ResizeEvent 36 | */ 37 | onResizeEnd(event: ResizeEvent): void { 38 | if (event.rectangle.height <= 10) { 39 | // Close overlay when swiped to the bottom of the screen 40 | this.onCloseOverlay(); 41 | this.overlayHeight = this.overlayMinHeight; 42 | } else { 43 | // Change overlay height 44 | this.overlayHeight = (event.rectangle.height < this.overlayMinHeight) ? this.overlayMinHeight : event.rectangle.height; 45 | this.overlayResized.emit(this.overlayHeight); 46 | this.overlay.nativeElement.style.height = this.overlayHeight + 'px'; 47 | } 48 | } 49 | 50 | /** 51 | * Close overlay 52 | */ 53 | onCloseOverlay() { 54 | this.overlayClosed.emit(); 55 | } 56 | 57 | /** 58 | * Checks the URL, if overlay should be closed 59 | * This function is needed to close the overlay if the user clicks the "back" button of the browser 60 | * and navigates back to component "/map" 61 | */ 62 | checkOverlayState() { 63 | if (this.router.url.search(/\/map$/) > -1) { 64 | this.onCloseOverlay(); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/app/map/tools/tool-print-editor/tool-print-editor.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | arrow_back 4 | Speichern und zurück 5 | 6 | -------------------------------------------------------------------------------- /src/app/map/tools/tool-print-editor/tool-print-editor.component.scss: -------------------------------------------------------------------------------- 1 | #print-editor-button { 2 | display: flex; 3 | width: unset; 4 | height: unset; 5 | padding-left: 8px; 6 | padding-right: 10px; 7 | align-items: center; 8 | } 9 | #wrapper { 10 | position: absolute; 11 | top: 10px; 12 | left: 10px 13 | } 14 | #print-editor-button-text { 15 | font-size: 18px; 16 | padding-left: 4px; 17 | color: rgba(0, 0, 0, 0.87); 18 | } -------------------------------------------------------------------------------- /src/app/map/tools/tool-print-editor/tool-print-editor.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { MapSavingService } from './../../map-saving.service'; 2 | import { ToolPrintEditorComponent } from './tool-print-editor.component'; 3 | import { MapStylingService } from './../../map-styling.service'; 4 | import { AppConfigService } from 'src/app/app-config.service'; 5 | import { RouterTestingModule } from '@angular/router/testing'; 6 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 7 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 8 | 9 | describe('ToolPrintEditorComponent', () => { 10 | let component: ToolPrintEditorComponent; 11 | let fixture: ComponentFixture; 12 | 13 | beforeEach(waitForAsync(() => { 14 | TestBed.configureTestingModule({ 15 | declarations: [ ToolPrintEditorComponent ], 16 | providers: [ 17 | {provide: MapStylingService, useClass: MapStylingServiceStub}, 18 | {provide: MapSavingService, useClass: MapSavingServiceStub}, 19 | {provide: AppConfigService, useClass: AppConfigServiceStub} 20 | ], 21 | imports: [ HttpClientTestingModule, RouterTestingModule ] 22 | }) 23 | .compileComponents(); 24 | })); 25 | 26 | beforeEach(() => { 27 | fixture = TestBed.createComponent(ToolPrintEditorComponent); 28 | component = fixture.componentInstance; 29 | fixture.detectChanges(); 30 | }); 31 | 32 | it('should create', () => { 33 | expect(component).toBeTruthy(); 34 | }); 35 | }); 36 | 37 | class MapStylingServiceStub {} 38 | class MapSavingServiceStub {} 39 | class AppConfigServiceStub {} 40 | -------------------------------------------------------------------------------- /src/app/map/tools/tool-print-editor/tool-print-editor.component.ts: -------------------------------------------------------------------------------- 1 | import { AppConfigService } from './../../../app-config.service'; 2 | import { MapSavingService } from './../../map-saving.service'; 3 | import { MapStylingService } from 'src/app/map/map-styling.service'; 4 | import { Component, OnInit } from '@angular/core'; 5 | 6 | @Component({ 7 | selector: 'app-tool-print-editor', 8 | templateUrl: './tool-print-editor.component.html', 9 | styleUrls: ['./tool-print-editor.component.scss'] 10 | }) 11 | export class ToolPrintEditorComponent implements OnInit { 12 | 13 | constructor( 14 | public mapStylingService: MapStylingService, 15 | private appConfigService: AppConfigService, 16 | private mapSavingService: MapSavingService) { } 17 | 18 | ngOnInit() {} 19 | 20 | returnToPrintEditor() { 21 | this.mapSavingService.saveMap().subscribe((response: any) => { 22 | const url = window.location.origin 23 | + this.appConfigService.settings.printEditor.url + '/' 24 | + '?lat=' + this.mapStylingService.mapView.center[1] 25 | + '&lng=' + this.mapStylingService.mapView.center[0] 26 | + '&zoom=' + this.mapStylingService.mapView.zoom 27 | + '&style=' + window.location.origin + this.appConfigService.settings.mapService.url + '/style/' + response.id; 28 | window.location.href = url; 29 | }); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/app/map/tools/tool-share/tool-share.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Anwendung 5 | 6 | 7 | 8 | content_copy 9 | 10 | 11 | code 12 | 13 | 14 | 15 | 16 | 17 | {{appUrl}} 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | Styling (JSON) 28 | 29 | 30 | 31 | content_copy 32 | 33 | 34 | 35 | 36 | 37 | {{stylingUrl}} 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | Editor 48 | 49 | 50 | 51 | content_copy 52 | 53 | 54 | 55 | 56 | 57 | {{editorUrl}} 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/app/map/tools/tool-share/tool-share.component.scss: -------------------------------------------------------------------------------- 1 | mat-divider { 2 | margin: 10px 0; 3 | } 4 | 5 | button { 6 | margin-right: 10px; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/map/tools/tool-share/tool-share.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { ToolShareComponent } from './tool-share.component'; 3 | import {HeaderService} from '../../../header/header.service'; 4 | import {MapStylingService} from '../../map-styling.service'; 5 | import {MapFunctionService} from '../../map-function.service'; 6 | import {HttpClientTestingModule} from '@angular/common/http/testing'; 7 | import {MatSnackBar} from '@angular/material/snack-bar'; 8 | import {AppConfigService} from '../../../app-config.service'; 9 | import {EventEmitter} from '@angular/core'; 10 | import {MaterialDesignModule} from '../../../material-design/material-design.module'; 11 | import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; 12 | import {ClipboardModule} from 'ngx-clipboard'; 13 | import {Location} from '@angular/common'; 14 | 15 | describe('ToolShareComponent', () => { 16 | let component: ToolShareComponent; 17 | let fixture: ComponentFixture; 18 | 19 | beforeEach(waitForAsync(() => { 20 | TestBed.configureTestingModule({ 21 | declarations: [ToolShareComponent], 22 | imports: [ 23 | HttpClientTestingModule, 24 | MaterialDesignModule, 25 | BrowserAnimationsModule, 26 | ClipboardModule 27 | ], 28 | providers: [ 29 | {provide: HeaderService, useClass: HeaderServiceStub}, 30 | {provide: MapStylingService, useClass: MapStylingServiceStub}, 31 | {provide: MapFunctionService, useClass: MapFunctionServiceStub}, 32 | {provide: MatSnackBar, useValue: {}}, 33 | {provide: AppConfigService, useClass: AppConfigServiceStub} 34 | ] 35 | }) 36 | .compileComponents(); 37 | })); 38 | 39 | beforeEach(() => { 40 | fixture = TestBed.createComponent(ToolShareComponent); 41 | component = fixture.componentInstance; 42 | fixture.detectChanges(); 43 | }); 44 | 45 | it('should create', () => { 46 | expect(component).toBeTruthy(); 47 | }); 48 | }); 49 | 50 | class HeaderServiceStub{ 51 | changeTitle(){} 52 | } 53 | class MapStylingServiceStub{ 54 | activeStylingChanged = new EventEmitter(); 55 | } 56 | class MapFunctionServiceStub{} 57 | class AppConfigServiceStub{ 58 | settings = { 59 | mapService: { 60 | url: {} 61 | } 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /src/app/map/tools/tool-share/tool-share.component.ts: -------------------------------------------------------------------------------- 1 | import { MapSavingService } from './../../map-saving.service'; 2 | import { Component, OnDestroy, OnInit } from '@angular/core'; 3 | import { HeaderService } from 'src/app/header/header.service'; 4 | import { MatSnackBar } from '@angular/material/snack-bar'; 5 | import { MapStylingService } from '../../map-styling.service'; 6 | import { AppConfigService } from 'src/app/app-config.service'; 7 | 8 | /** 9 | * Component to save and share the map stylings and configuration 10 | */ 11 | @Component({ 12 | selector: 'app-tool-share', 13 | templateUrl: './tool-share.component.html', 14 | styleUrls: ['./tool-share.component.scss'] 15 | }) 16 | export class ToolShareComponent implements OnInit, OnDestroy { 17 | 18 | stylingUrl: string; 19 | appUrl: string; 20 | appIframe: string; 21 | editorUrl: string; 22 | activeStyling: any; 23 | deepLoad: any; 24 | 25 | constructor(private headerService: HeaderService, 26 | private mapStylingService: MapStylingService, 27 | private snackBar: MatSnackBar, 28 | private appConfigService: AppConfigService, 29 | private mapSavingService: MapSavingService 30 | ) { } 31 | 32 | ngOnInit() { 33 | this.headerService.changeTitle('Karte veröffentlichen'); 34 | this.activeStyling = this.mapStylingService.activeStyling; 35 | this.activeStyling && this.getSharingUrl(); 36 | // listen when active styling is loaded if deep link is used 37 | this.deepLoad = this.mapStylingService.activeStylingChanged.subscribe( () => { 38 | this.activeStyling = this.mapStylingService.activeStyling; 39 | this.getSharingUrl(); 40 | }); 41 | } 42 | 43 | ngOnDestroy() { 44 | this.deepLoad.unsubscribe(); 45 | } 46 | /** 47 | * Build and display sharing urls 48 | */ 49 | getSharingUrl(){ 50 | this.mapSavingService.saveMap().subscribe((response: any) => { 51 | this.stylingUrl = this.completeUrl(this.appConfigService.settings.mapService.url + '/style/' + response.id); 52 | this.appUrl = this.completeUrl(this.appConfigService.settings.mapView.url + '/' + response.id); 53 | this.appIframe = ''; 54 | const mapPageUrl = window.location.protocol + '//' + window.location.host + 55 | window.location.pathname.substring(0, window.location.pathname.search('/map/') + 4); 56 | this.editorUrl = mapPageUrl + '?id=' + response.id; 57 | }); 58 | } 59 | 60 | /** 61 | * Show success message 62 | */ 63 | onCopySuccess() { 64 | this.snackBar.open('URL wurde in die Zwischenablage kopiert.', '', { 65 | duration: 2000 66 | }); 67 | } 68 | 69 | /** 70 | * Prepends protocol and host to a relative URL 71 | * 72 | * @param url: Relative or absolute URL 73 | */ 74 | completeUrl(url: string) { 75 | if (url.indexOf('/') === 0) { 76 | url = window.location.protocol + '//' + window.location.host + url; 77 | } 78 | return url; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/app/material-design/material-design.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { MatButtonModule } from '@angular/material/button'; 3 | import { MatCardModule } from '@angular/material/card'; 4 | import { MatCheckboxModule } from '@angular/material/checkbox'; 5 | import { MatDividerModule } from '@angular/material/divider'; 6 | import { MatExpansionModule } from '@angular/material/expansion'; 7 | import { MatIconModule } from '@angular/material/icon'; 8 | import { MatInputModule } from '@angular/material/input'; 9 | import { MatListModule } from '@angular/material/list'; 10 | import { MatSidenavModule } from '@angular/material/sidenav'; 11 | import { MatSlideToggleModule } from '@angular/material/slide-toggle'; 12 | import { MatSliderModule } from '@angular/material/slider'; 13 | import { MatSnackBarModule } from '@angular/material/snack-bar'; 14 | import { MatToolbarModule } from '@angular/material/toolbar'; 15 | 16 | /** 17 | * Module for import of Angular Material Modules 18 | */ 19 | @NgModule({ 20 | exports: [ 21 | MatButtonModule, 22 | MatDividerModule, 23 | MatIconModule, 24 | MatListModule, 25 | MatSidenavModule, 26 | MatToolbarModule, 27 | MatCardModule, 28 | MatExpansionModule, 29 | MatCheckboxModule, 30 | MatSliderModule, 31 | MatSlideToggleModule, 32 | MatSnackBarModule, 33 | MatInputModule 34 | ] 35 | }) 36 | export class MaterialDesignModule {} 37 | -------------------------------------------------------------------------------- /src/app/material-design/vt-map-editor-material-theme.scss: -------------------------------------------------------------------------------- 1 | @import '~@angular/material/theming'; 2 | 3 | @include mat-core(); 4 | 5 | $palette-primary: ( 6 | 50: #fdebf0, 7 | 100: #fbccd7, 8 | 200: #ea97a3, 9 | 300: #e06f80, 10 | 400: #ec4a62, 11 | 500: #f3324c, 12 | 600: #e3284a, 13 | 700: #d11e42, 14 | 800: #c4153a, 15 | 900: #b5002f, 16 | A100: #fdebf0, 17 | A200: #f3324c, 18 | A400: #c4153a, 19 | A700: #b5002f, 20 | contrast: ( 21 | 50: $dark-primary-text, 22 | 100: $dark-primary-text, 23 | 200: $dark-primary-text, 24 | 300: $dark-primary-text, 25 | 400: $light-primary-text, 26 | 500: $light-primary-text, 27 | 600: $light-primary-text, 28 | 700: $light-primary-text, 29 | 800: $light-primary-text, 30 | 900: $light-primary-text, 31 | A100: $dark-primary-text, 32 | A200: $light-primary-text, 33 | A400: $light-primary-text, 34 | A700: $light-primary-text, 35 | ) 36 | ); 37 | 38 | $palette-accent: ( 39 | 50: #f8fafe, 40 | 100: #f3f5f9, 41 | 200: #eceef2, 42 | 300: #dee0e4, 43 | 400: #bbbdc0, 44 | 500: #9c9ea1, 45 | 600: #737578, 46 | 700: #5f6164, 47 | 800: #404245, 48 | 900: #1f2124, 49 | A100: #f3f5f9, 50 | A200: #bbbdc0, 51 | A400: #5f6164, 52 | A700: #1f2124, 53 | contrast: ( 54 | 50: $dark-primary-text, 55 | 100: $dark-primary-text, 56 | 200: $dark-primary-text, 57 | 300: $dark-primary-text, 58 | 400: $light-primary-text, 59 | 500: $light-primary-text, 60 | 600: $light-primary-text, 61 | 700: $light-primary-text, 62 | 800: $light-primary-text, 63 | 900: $light-primary-text, 64 | A100: $dark-primary-text, 65 | A200: $light-primary-text, 66 | A400: $light-primary-text, 67 | A700: $light-primary-text, 68 | ) 69 | ); 70 | 71 | 72 | $primary: mat-palette($palette-primary, A400, A200, A700); 73 | $accent: mat-palette($palette-accent, A400, A200, A700); 74 | $warn: mat-palette($mat-red); 75 | 76 | $vt-map-editor-theme: mat-light-theme($primary, $accent, $warn); 77 | 78 | @include angular-material-theme($vt-map-editor-theme); 79 | -------------------------------------------------------------------------------- /src/app/menu/menu-item.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Entry in sidebar menu 3 | */ 4 | export class MenuItem { 5 | constructor( 6 | public label: string, 7 | public icon: string, 8 | public link: string, 9 | public externalLink: boolean 10 | ) {} 11 | } 12 | -------------------------------------------------------------------------------- /src/app/menu/menu.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{title}} 7 | 8 | 9 | 10 | 11 | {{version}} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {{item.icon}} 26 | {{item.label}} 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/app/menu/menu.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles.scss'; 2 | 3 | .menu-header { 4 | padding: 20px; 5 | min-width: 200px; 6 | font-weight: bold; 7 | 8 | .repo-logo { 9 | width: 32px; 10 | } 11 | } 12 | 13 | .menu-logo { 14 | margin-top: 20px; 15 | padding: 10px 20px; 16 | } 17 | -------------------------------------------------------------------------------- /src/app/menu/menu.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { MenuItem } from './menu-item'; 3 | import { MenuComponent } from './menu.component'; 4 | import {HttpClientTestingModule} from '@angular/common/http/testing'; 5 | import {AppConfigService} from '../app-config.service'; 6 | import {RouterTestingModule} from '@angular/router/testing'; 7 | import {MaterialDesignModule} from '../material-design/material-design.module'; 8 | import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; 9 | 10 | describe('MenuComponent', () => { 11 | let component: MenuComponent; 12 | let fixture: ComponentFixture; 13 | 14 | beforeEach(waitForAsync(() => { 15 | TestBed.configureTestingModule({ 16 | declarations: [MenuComponent], 17 | imports: [ 18 | RouterTestingModule, 19 | MaterialDesignModule, 20 | BrowserAnimationsModule, 21 | HttpClientTestingModule 22 | ], 23 | providers: [ 24 | {provide: AppConfigService, useClass: AppConfigServiceStub} 25 | ] 26 | }).compileComponents(); 27 | })); 28 | 29 | beforeEach(() => { 30 | fixture = TestBed.createComponent(MenuComponent); 31 | component = fixture.componentInstance; 32 | fixture.detectChanges(); 33 | }); 34 | 35 | it('should create', () => { 36 | expect(component).toBeTruthy(); 37 | }); 38 | }); 39 | class AppConfigServiceStub{ 40 | settings = {menuItems: [ 41 | { 42 | label: 'Map', 43 | icon: 'map', 44 | link: 'map', 45 | externalLink: false 46 | }, 47 | { 48 | label: 'Privacy', 49 | icon: 'lock', 50 | link: 'privacy', 51 | externalLink: false 52 | }, 53 | { 54 | label: 'Legals', 55 | icon: 'comment', 56 | link: 'https://...', 57 | externalLink: true 58 | } 59 | ], 60 | 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /src/app/menu/menu.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; 2 | import { AppConfigService } from '../app-config.service'; 3 | import { MenuItem } from './menu-item'; 4 | 5 | /** 6 | * Sidebar menu 7 | */ 8 | @Component({ 9 | selector: 'app-menu', 10 | templateUrl: './menu.component.html', 11 | styleUrls: ['./menu.component.scss'] 12 | }) 13 | export class MenuComponent implements OnInit { 14 | @Input() title: string; 15 | @Input() version: string; 16 | @Output() itemSelected = new EventEmitter(); 17 | 18 | menuItems: MenuItem[]; 19 | 20 | constructor( 21 | private appConfigService: AppConfigService 22 | ) { 23 | // Read menu items definitions from configuration 24 | if (appConfigService.settings.menuItems !== undefined) { 25 | this.menuItems = appConfigService.settings.menuItems; 26 | } 27 | } 28 | 29 | ngOnInit() { } 30 | 31 | /** 32 | * Select menu item 33 | * @param item selected MenuItem 34 | * @emit MenuComponent#itemSelected 35 | */ 36 | onItemSelected(item: MenuItem) { 37 | this.itemSelected.emit(item); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/app/shared/basemap.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Basemap for map style 3 | */ 4 | export class Basemap { 5 | constructor( 6 | public name: string, 7 | public imgUrl: string, 8 | public styling: string, 9 | public description?: string, 10 | public randomColors?: boolean, 11 | public metadataFile?: string 12 | ) { } 13 | } 14 | -------------------------------------------------------------------------------- /src/app/shared/map-function.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Item in map function configurations 3 | */ 4 | export class MapFunction { 5 | constructor( 6 | public show: boolean, 7 | public enabled: boolean, 8 | public configuration: any 9 | ) {} 10 | } 11 | -------------------------------------------------------------------------------- /src/app/shared/map-functions.ts: -------------------------------------------------------------------------------- 1 | import { MapFunction } from './map-function'; 2 | 3 | /** 4 | * Array of map functions 5 | */ 6 | export class MapFunctions { 7 | constructor( 8 | public navigation: MapFunction, 9 | public info: MapFunction, 10 | public search: MapFunction 11 | ) {} 12 | } 13 | -------------------------------------------------------------------------------- /src/app/shared/mapview.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Map View Parameters 3 | */ 4 | export class MapView { 5 | constructor( 6 | public zoom: number, 7 | public center: number[], 8 | public pitch: number, 9 | public bearing: number 10 | ) { } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/shared/settings.model.ts: -------------------------------------------------------------------------------- 1 | import { MenuItem } from '../menu/menu-item'; 2 | import { Basemap } from './basemap'; 3 | import { MapFunctions } from './map-functions'; 4 | 5 | /** 6 | * Model of setting parameters 7 | */ 8 | export interface SettingsModel { 9 | titles: { 10 | map: string 11 | }; 12 | mapService: { 13 | url: string; 14 | }; 15 | mapView: { 16 | url: string; 17 | }; 18 | printEditor: { 19 | url: string; 20 | }; 21 | map: { 22 | maxZoom: number; 23 | startCenter: number[]; 24 | startZoom: number; 25 | showZoomLevel: boolean; 26 | showScaleBar: boolean; 27 | }; 28 | mapFunctions: MapFunctions; 29 | basemaps: Basemap[]; 30 | menuItems: MenuItem[]; 31 | guiLayers: { 32 | sortByName: boolean; 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/assets/config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "titles": { 3 | "map": "Basisvisualisierung" 4 | }, 5 | "mapService": { 6 | "url": "/map-service" 7 | }, 8 | "mapView": { 9 | "url": "/map-view" 10 | }, 11 | "printEditor": { 12 | "url": "/print-editor" 13 | }, 14 | "map": { 15 | "maxZoom": 18, 16 | "startCenter": [9.732934, 52.373292], 17 | "startZoom": 12, 18 | "showZoomLevel": false, 19 | "showScaleBar": true 20 | }, 21 | "mapFunctions": { 22 | "navigation": { 23 | "show": true, 24 | "enabled": true 25 | }, 26 | "info": { 27 | "show": true, 28 | "enabled": true 29 | }, 30 | "search": { 31 | "show": true, 32 | "enabled": true 33 | } 34 | }, 35 | "basemaps": [ 36 | { 37 | "name": "Farbe", 38 | "imgUrl": "assets/images/thumbnails/basemap_color.png", 39 | "styling": "/styles/vt-style-color.json" 40 | }, 41 | { 42 | "name": "Klassisch", 43 | "imgUrl": "assets/images/thumbnails/basemap_classic.png", 44 | "styling": "/styles/vt-style-classic.json" 45 | }, 46 | { 47 | "name": "Graustufen", 48 | "imgUrl": "assets/images/thumbnails/basemap_grayscale.png", 49 | "styling": "/styles/vt-style-grayscale.json" 50 | }, 51 | { 52 | "name": "Hell", 53 | "imgUrl": "assets/images/thumbnails/basemap_light.png", 54 | "styling": "/styles/vt-style-light.json" 55 | }, 56 | { 57 | "name": "Nacht", 58 | "imgUrl": "assets/images/thumbnails/basemap_night.png", 59 | "styling": "/styles/vt-style-night.json" 60 | }, 61 | { 62 | "name": "Zufall", 63 | "imgUrl": "assets/images/thumbnails/basemap_random.png", 64 | "styling": "", 65 | "randomColors": true 66 | } 67 | ], 68 | "menuItems": [ 69 | { 70 | "label": "Karte", 71 | "icon": "map", 72 | "link": "map", 73 | "externalLink": false 74 | }, 75 | { 76 | "label": "Datenschutz", 77 | "icon": "lock", 78 | "link": "https://www.lgln.niedersachsen.de/startseite/wir_uber_uns_amp_organisation/datenschutz/datenschutz-im-lgln-138166.html", 79 | "externalLink": true 80 | }, 81 | { 82 | "label": "Impressum", 83 | "icon": "comment", 84 | "link": "https://www.lgln.niedersachsen.de/startseite/wir_uber_uns_amp_organisation/impressum/impressum-92629.html", 85 | "externalLink": true 86 | } 87 | ], 88 | "guiLayers": { 89 | "sortByName": true 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/assets/images/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Basisvisualisierung/vt-map-editor/20daf75105c50a236f4cd4664c7c28445131bfcc/src/assets/images/github.png -------------------------------------------------------------------------------- /src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Basisvisualisierung/vt-map-editor/20daf75105c50a236f4cd4664c7c28445131bfcc/src/assets/images/logo.png -------------------------------------------------------------------------------- /src/assets/images/thumbnails/basemap_classic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Basisvisualisierung/vt-map-editor/20daf75105c50a236f4cd4664c7c28445131bfcc/src/assets/images/thumbnails/basemap_classic.png -------------------------------------------------------------------------------- /src/assets/images/thumbnails/basemap_color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Basisvisualisierung/vt-map-editor/20daf75105c50a236f4cd4664c7c28445131bfcc/src/assets/images/thumbnails/basemap_color.png -------------------------------------------------------------------------------- /src/assets/images/thumbnails/basemap_grayscale.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Basisvisualisierung/vt-map-editor/20daf75105c50a236f4cd4664c7c28445131bfcc/src/assets/images/thumbnails/basemap_grayscale.png -------------------------------------------------------------------------------- /src/assets/images/thumbnails/basemap_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Basisvisualisierung/vt-map-editor/20daf75105c50a236f4cd4664c7c28445131bfcc/src/assets/images/thumbnails/basemap_light.png -------------------------------------------------------------------------------- /src/assets/images/thumbnails/basemap_load.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Basisvisualisierung/vt-map-editor/20daf75105c50a236f4cd4664c7c28445131bfcc/src/assets/images/thumbnails/basemap_load.png -------------------------------------------------------------------------------- /src/assets/images/thumbnails/basemap_night.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Basisvisualisierung/vt-map-editor/20daf75105c50a236f4cd4664c7c28445131bfcc/src/assets/images/thumbnails/basemap_night.png -------------------------------------------------------------------------------- /src/assets/images/thumbnails/basemap_random.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Basisvisualisierung/vt-map-editor/20daf75105c50a236f4cd4664c7c28445131bfcc/src/assets/images/thumbnails/basemap_random.png -------------------------------------------------------------------------------- /src/assets/templates/legals.html: -------------------------------------------------------------------------------- 1 | Hier Text zum Impressum einfügen. 2 | -------------------------------------------------------------------------------- /src/assets/templates/privacy.html: -------------------------------------------------------------------------------- 1 | Hier Text zum Datenschutz einfügen. 2 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Basisvisualisierung/vt-map-editor/20daf75105c50a236f4cd4664c7c28445131bfcc/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | VT Map Editor 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../coverage/vt-map-editor'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags.ts'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | @import 'app/material-design/vt-map-editor-material-theme.scss'; 2 | 3 | $header-height: 56px; 4 | 5 | body { 6 | font-family: Roboto, Helvetica Neue, sans-serif; 7 | overflow: hidden; 8 | margin: 0; 9 | padding: 0; 10 | background-color: #FFFFFF; 11 | } 12 | 13 | .primary { 14 | color: map-get($primary, A400); 15 | } 16 | 17 | .bg-primary { 18 | background-color: map-get($primary, A400); 19 | color: mat-contrast($primary, A400); 20 | } 21 | 22 | .accent { 23 | color: map-get($accent, A400); 24 | } 25 | 26 | .bg-accent { 27 | background-color: map-get($accent, A400); 28 | color: mat-contrast($accent, A400) 29 | } 30 | 31 | .color-picker * { 32 | font-size: 22px !important; 33 | 34 | input { 35 | height: 32px !important; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /src/testing/DOMHelper.ts: -------------------------------------------------------------------------------- 1 | import {ComponentFixture} from '@angular/core/testing'; 2 | import {ToolBasemapComponent} from '../app/map/tools/tool-basemap/tool-basemap.component'; 3 | import {By} from '@angular/platform-browser'; 4 | 5 | export class DOMHelper { 6 | private fixture: ComponentFixture; 7 | 8 | constructor(fixture: ComponentFixture) { 9 | this.fixture = fixture; 10 | } 11 | 12 | getCollection(tagName: string) { 13 | return this.fixture.debugElement.queryAll(By.css(tagName)); 14 | } 15 | 16 | getNativeButton(tagName: string, position: number = 0): HTMLButtonElement { 17 | this.fixture.detectChanges(); 18 | const nativeButton = this.fixture.debugElement.queryAll(By.css(tagName))[position].nativeElement; 19 | return nativeButton && nativeButton; 20 | } 21 | 22 | getLength(tagName: string): number { 23 | this.fixture.detectChanges(); 24 | const elementLength = this.fixture.debugElement.queryAll(By.css(tagName)).length; 25 | return elementLength && elementLength; 26 | } 27 | 28 | addValue(tagName: string, position: number = 0, value: string = 'test value'){ 29 | this.fixture.debugElement.queryAll(By.css(tagName))[position].nativeElement.value = value; 30 | } 31 | 32 | clickButton(buttonText: string): void { 33 | for (const button of this.getCollection('button')){ 34 | const nativeButton: HTMLButtonElement = button.nativeElement; 35 | if (nativeButton.textContent === buttonText){ 36 | nativeButton.click(); 37 | } 38 | } 39 | } 40 | 41 | clickElement(tagName: string, textContent: string) { 42 | for (const element of this.getCollection(tagName)){ 43 | const nativeElement: HTMLElement = element.nativeElement; 44 | if (nativeElement.textContent.trim() === textContent){ 45 | nativeElement.click(); 46 | } 47 | } 48 | } 49 | 50 | clickAll(tagName: string) { 51 | for (const element of this.getCollection(tagName)) { 52 | const nativeElement: HTMLElement = element.nativeElement; 53 | nativeElement.click(); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/testing/activated-route-stub.ts: -------------------------------------------------------------------------------- 1 | import { convertToParamMap, ParamMap, Params, ActivatedRoute } from '@angular/router'; 2 | import { ReplaySubject } from 'rxjs'; 3 | 4 | /** 5 | * An ActivateRoute test double with a `paramMap` observable. 6 | * Use the `setParamMap()` method to add the next `paramMap` value. 7 | */ 8 | export class ActivatedRouteStub { 9 | // Use a ReplaySubject to share previous values with subscribers 10 | // and pump new values into the `paramMap` observable 11 | private subject = new ReplaySubject(); 12 | 13 | constructor(initialParams?: Params) { 14 | this.setParamMap(initialParams); 15 | } 16 | 17 | /** The mock paramMap observable */ 18 | readonly paramMap = this.subject.asObservable(); 19 | 20 | /** Set the paramMap observables's next value */ 21 | setParamMap(params?: Params) { 22 | this.subject.next(convertToParamMap(params)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "main.ts", 9 | "polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "test.ts", 12 | "polyfills.ts" 13 | ], 14 | "include": [ 15 | "**/*.spec.ts", 16 | "**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "allowSyntheticDefaultImports": true, 5 | "baseUrl": "./", 6 | "downlevelIteration": true, 7 | "outDir": "./dist/out-tsc", 8 | "sourceMap": true, 9 | "declaration": false, 10 | "module": "es2020", 11 | "moduleResolution": "node", 12 | "emitDecoratorMetadata": true, 13 | "experimentalDecorators": true, 14 | "importHelpers": true, 15 | "target": "es2015", 16 | "typeRoots": [ 17 | "node_modules/@types" 18 | ], 19 | "lib": [ 20 | "es2018", 21 | "dom" 22 | ] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rulesDirectory": [ 4 | "codelyzer" 5 | ], 6 | "rules": { 7 | "align": { 8 | "options": [ 9 | "parameters", 10 | "statements" 11 | ] 12 | }, 13 | "no-unused-expression": [true, "allow-fast-null-checks"], 14 | "array-type": false, 15 | "arrow-parens": false, 16 | "arrow-return-shorthand": true, 17 | "curly": true, 18 | "deprecation": { 19 | "severity": "warn" 20 | }, 21 | "eofline": true, 22 | "import-blacklist": [ 23 | true, 24 | "rxjs/Rx" 25 | ], 26 | "import-spacing": true, 27 | "indent": { 28 | "options": [ 29 | "spaces" 30 | ] 31 | }, 32 | "interface-name": false, 33 | "max-classes-per-file": false, 34 | "max-line-length": [ 35 | true, 36 | 140 37 | ], 38 | "member-access": false, 39 | "member-ordering": [ 40 | true, 41 | { 42 | "order": [ 43 | "static-field", 44 | "instance-field", 45 | "static-method", 46 | "instance-method" 47 | ] 48 | } 49 | ], 50 | "no-consecutive-blank-lines": false, 51 | "no-console": [ 52 | true, 53 | "debug", 54 | "info", 55 | "time", 56 | "timeEnd", 57 | "trace" 58 | ], 59 | "no-empty": false, 60 | "no-inferrable-types": [ 61 | true, 62 | "ignore-params" 63 | ], 64 | "no-non-null-assertion": true, 65 | "no-redundant-jsdoc": true, 66 | "no-switch-case-fall-through": true, 67 | "no-var-requires": false, 68 | "object-literal-key-quotes": [ 69 | true, 70 | "as-needed" 71 | ], 72 | "object-literal-sort-keys": false, 73 | "object-literal-shorthand": [ 74 | true, 75 | "never" 76 | ], 77 | "ordered-imports": false, 78 | "quotemark": [ 79 | true, 80 | "single" 81 | ], 82 | "semicolon": { 83 | "options": [ 84 | "always" 85 | ] 86 | }, 87 | "space-before-function-paren": { 88 | "options": { 89 | "anonymous": "never", 90 | "asyncArrow": "always", 91 | "constructor": "never", 92 | "method": "never", 93 | "named": "never" 94 | } 95 | }, 96 | "trailing-comma": false, 97 | "no-output-on-prefix": true, 98 | "no-inputs-metadata-property": true, 99 | "no-outputs-metadata-property": true, 100 | "no-host-metadata-property": true, 101 | "no-input-rename": true, 102 | "no-output-rename": true, 103 | "typedef-whitespace": { 104 | "options": [ 105 | { 106 | "call-signature": "nospace", 107 | "index-signature": "nospace", 108 | "parameter": "nospace", 109 | "property-declaration": "nospace", 110 | "variable-declaration": "nospace" 111 | }, 112 | { 113 | "call-signature": "onespace", 114 | "index-signature": "onespace", 115 | "parameter": "onespace", 116 | "property-declaration": "onespace", 117 | "variable-declaration": "onespace" 118 | } 119 | ] 120 | }, 121 | "use-lifecycle-interface": true, 122 | "use-pipe-transform-interface": true, 123 | "component-class-suffix": true, 124 | "directive-class-suffix": true, 125 | "variable-name": { 126 | "options": [ 127 | "ban-keywords", 128 | "check-format", 129 | "allow-pascal-case" 130 | ] 131 | }, 132 | "whitespace": { 133 | "options": [ 134 | "check-branch", 135 | "check-decl", 136 | "check-operator", 137 | "check-separator", 138 | "check-type", 139 | "check-typecast" 140 | ] 141 | } 142 | } 143 | } 144 | --------------------------------------------------------------------------------
Speichern und zurück