├── .gitignore ├── CONTRIBUTING.md ├── Gruntfile.js ├── README.md ├── building-viewer.png ├── docs ├── NameConventions.md ├── Quickstart.md ├── Sections.md └── images │ ├── screenshot_1.png │ ├── screenshot_2.png │ ├── screenshot_3.png │ ├── screenshot_4.png │ ├── screenshot_5.png │ ├── screenshot_6.png │ ├── screenshot_7.png │ └── screenshot_8.png ├── index.html ├── license.txt ├── package-lock.json ├── package.json ├── src ├── css │ ├── main.scss │ ├── mixins.scss │ └── variables.scss └── js │ ├── AppState.ts │ ├── BuildingViewer.tsx │ ├── config.tsx │ ├── sections │ ├── FloorsSection.tsx │ ├── HomeSection.tsx │ ├── Section.ts │ ├── Sections.tsx │ ├── SurroundingsSection.tsx │ └── css │ │ └── sections.scss │ ├── support │ ├── BuildingVisualisation.ts │ ├── SurroundingsVisualisation.ts │ ├── appUtils.ts │ ├── buildingSceneLayerUtils.ts │ └── visualVariables.ts │ └── widgets │ ├── FloorSelector │ ├── FloorSelector.tsx │ └── css │ │ └── floorSelector.scss │ ├── Popup │ ├── Popup.tsx │ ├── PopupInfo.ts │ └── css │ │ └── popup.scss │ ├── Timetable │ ├── Timetable.tsx │ └── css │ │ └── timetable.scss │ ├── Toggle │ ├── Toggle.tsx │ └── css │ │ └── toggle.scss │ ├── Viewpoints │ ├── OneViewpoint.tsx │ ├── Viewpoints.tsx │ └── css │ │ └── viewpoints.scss │ └── widgets.scss ├── tsconfig.json ├── tslint.json └── typings └── arcgis-js-api-4.20.d.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | src/**/*.js* 3 | src/**/*.css 4 | *.css.map 5 | css/**/*.css* 6 | .sass-cache/* 7 | dist/ 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing to ArcGIS API for JavaScript 2 | 3 | This guide describes how you can contribute code improvements to the ArcGIS API for JavaScript. 4 | 5 | 1. Review the [README](README.md). 6 | 2. Create a new feature branch in your local repo. 7 | * The name of the branch doesn't matter, but as a best practice use a descriptive name like "add-annotation-layer". 8 | 3. Write code to add an enhancement or fix the problem. 9 | 4. Test your code. 10 | 11 | ### Submitting changes 12 | 13 | 1. Push your feature branch to the repo or your fork of the repo if you don't have push access. 14 | 2. Submit a [pull request](https://help.github.com/articles/using-pull-requests) against the "master" branch. 15 | * Clearly describe the issue including steps to reproduce; or if an enhancement, indicate the functionality you built. 16 | 17 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | var options = { 3 | livereload: true, 4 | port: grunt.option('port') || 8888, 5 | hostname: grunt.option('server') || "localhost", 6 | }; 7 | 8 | // Project configuration. 9 | grunt.initConfig({ 10 | watch: { 11 | livereload: { 12 | // Here we watch any files changed 13 | options: { 14 | livereload: true 15 | }, 16 | files: [ 17 | 'src/**/*.js', 18 | 'src/**/*.css', 19 | 'src/**/*.html' 20 | ] 21 | } 22 | }, 23 | connect: { 24 | app: { 25 | options: options 26 | } 27 | }, 28 | concat : { 29 | dist : { 30 | src : ['src/js/**/*.js'], 31 | dest : '.tmp/main.js' 32 | } 33 | }, 34 | uglify : { 35 | dist : { 36 | src : 'dist/main.min.js', 37 | dest : 'dist/main.min.js' 38 | } 39 | }, 40 | htmlmin: { 41 | dist: { 42 | options: { 43 | removeComments: true, 44 | collapseWhitespace: true 45 | }, 46 | files: { 47 | 'dist/index.html': 'dist/index.html' 48 | } 49 | } 50 | }, 51 | copy: { 52 | dist: { 53 | files: [ 54 | { 55 | expand: true, 56 | src: ['src/**/*.css'], 57 | dest: 'dist/' 58 | } 59 | ] 60 | } 61 | }, 62 | comments: { 63 | dist: { 64 | options: { 65 | singleline: true, 66 | multiline: true 67 | }, 68 | src: [ 'dist/*.html' ] 69 | } 70 | }, 71 | includeSource: { 72 | options: { 73 | basePath: 'dist' 74 | }, 75 | dist: { 76 | files: { 77 | 'dist/index.html': './index.html' 78 | } 79 | } 80 | }, 81 | run: { 82 | tscDist: { 83 | cmd: 'npx', 84 | args: [ 85 | 'tsc', 86 | '--outFile', 87 | 'dist/main.min.js' 88 | ] 89 | } 90 | } 91 | }); 92 | 93 | // dist 94 | grunt.loadNpmTasks('grunt-contrib-uglify'); 95 | grunt.loadNpmTasks('grunt-contrib-htmlmin'); 96 | grunt.loadNpmTasks('grunt-contrib-concat'); 97 | grunt.loadNpmTasks('grunt-contrib-copy'); 98 | grunt.loadNpmTasks('grunt-stripcomments'); 99 | grunt.loadNpmTasks('grunt-include-source'); 100 | grunt.loadNpmTasks('grunt-run'); 101 | 102 | 103 | // Load grunt plugins 104 | grunt.loadNpmTasks('grunt-contrib-watch'); 105 | grunt.loadNpmTasks('grunt-contrib-connect'); 106 | 107 | 108 | // Register tasks 109 | grunt.registerTask("default", ["connect", "watch"]); 110 | grunt.registerTask("dist", ["run:tscDist", "includeSource:dist", "copy:dist", "uglify:dist", "htmlmin:dist", "comments:dist"]); 111 | }; 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Building Viewer 2 | 3 | This demonstrates the use of [ArcGIS API 4 for JavaScript](https://developers.arcgis.com/javascript/) and [Building Scene Layers](https://developers.arcgis.com/javascript/latest/api-reference/) in a compelling website. 4 | 5 | The application presents the [Turanga library](https://my.christchurchcitylibraries.com/turanga/) in 3D. The visitor can explore the library by navigating around, and then inside, floor by floor, to discover this amazing building. 6 | 7 | [Visit the live website here.](https://esri.github.io/building-viewer) 8 | 9 | ![The live website](./docs/images/screenshot_1.png) 10 | 11 | 12 | ## Features 13 | * Building exploration - discover the great Turanga library 14 | * Customisation - use this app to display your own building 15 | * Discover the building floor by floor on a 2D visualisation 16 | * Get a broader perspective of the building surroundings 17 | 18 | ## Instructions 19 | 20 | ### To get a live copy on your machine 21 | 22 | 1. Clone the repo and `npm install` dependencies 23 | 2. Remove ref to this repo: `rm -rf .git` 24 | 3. `npm run build` to compile `src/js/*.ts` and `src/css/*.sccs` files in the same folder and watch for changes 25 | 4. `npm run server` launches a webserver. 26 | 5. Open your browser and enter the local address `http://localhost:8888/`. You should see now the Building Viewer running. 27 | 28 | ### To add your own building 29 | 30 | 1. Create a webscene with a BuildingSceneLayer named `Building 31 | 2. Open `src/config.tsx` in your favorite code editor 32 | 3. Delete all the content except the two first obscure lines 33 | 4. Now you need to define 2 parameters in the config to get started: 34 | 35 | The `websceneId` of the webscene you created above 36 | ``` 37 | export const websceneId = "YOUR WEBSCENE ID HERE"; 38 | ``` 39 | *Note that you may to also export on which portal this webscene resides if different from the ArcGis's portal: `export const portalUrl = "https://your-portal-url.com";`* 40 | 41 | The `sections` you'd like to have in your Building Viewer (see documentation about sections). Let's start with only one section, the home page: 42 | ```typescript 43 | // first import the section: 44 | import HomeSection = require("./sections/HomeSection"); 45 | 46 | // then export the `sections` parameter: 47 | export const sections = [ 48 | new HomeSection({}) 49 | ]; 50 | ``` 51 | 52 | 5. Recompile the code and reload the website. 53 | 54 | Checkout the documentation in the `docs` folder, and in particular the [quick start guide](./docs/Quickstart.md). 55 | 56 | ## Requirements 57 | 58 | * Notepad or your favorite HTML editor 59 | * `npm` and some knowledge of [Typescript](https://www.typescriptlang.org/) 60 | * Web browser with access to the Internet 61 | 62 | ## Resources 63 | 64 | The following external libraries, APIs, open datasets and specifications were used to make this application: 65 | 66 | * [ArcGIS API for JavaScript](https://developers.arcgis.com/javascript/) 67 | * [Calcite Web](http://esri.github.io/calcite-web/) 68 | * Turangua's BIM data provided by [Christchurch City Council](https://www.ccc.govt.nz/) 69 | * [Christchurch city model](https://www.linz.govt.nz/news/2014-03/3d-models-released-christchurch-city) provided by [Christchurch City Council](https://www.ccc.govt.nz/) 70 | * [Roboto font](https://fonts.google.com/specimen/Roboto) 71 | 72 | ## Issues 73 | 74 | Find a bug or want to request a new feature? Please let us know by submitting an issue. 75 | 76 | ## Contributing 77 | 78 | Esri welcomes contributions from anyone and everyone. Please see our [guidelines for contributing](https://github.com/esri/contributing). 79 | 80 | ## Licensing 81 | Copyright 2019 Esri 82 | 83 | Licensed under the Apache License, Version 2.0 (the "License"); 84 | you may not use this file except in compliance with the License. 85 | You may obtain a copy of the License at 86 | 87 | http://www.apache.org/licenses/LICENSE-2.0 88 | 89 | Unless required by applicable law or agreed to in writing, software 90 | distributed under the License is distributed on an "AS IS" BASIS, 91 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 92 | See the License for the specific language governing permissions and 93 | limitations under the License. 94 | 95 | A copy of the license is available in the repository's [license.txt](license.txt) file. 96 | -------------------------------------------------------------------------------- /building-viewer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/building-viewer/4b677fffb43b566e0f001e8b9fd04115ad078b9d/building-viewer.png -------------------------------------------------------------------------------- /docs/NameConventions.md: -------------------------------------------------------------------------------- 1 | # Naming conventions 2 | 3 | ## Layers 4 | 5 | Besides the required the building scene layer, without which the entire application would not have any sense, there are some layers and their names that have specific meaning. Any other layer you add to the scene will be left untouched and visible at all time in your application. 6 | 7 | All the names need to include the following, but you can have longer layer's name, e.g. "Building: Turanga Library". 8 | 9 | - `"Building"`: this is the only required layer in your webscene. It needs to be a [Building Scene Layer](https://developers.arcgis.com/javascript/latest/api-reference/) 10 | - `"City model"`: this is an optional layer in your webscene that show the surroundings. It needs to be a [Scene Layer](https://developers.arcgis.com/javascript/latest/api-reference/esri-layers-SceneLayer.html). 11 | - `"Floor points"`: this layer needs to be a [`FeatureLayer`](https://developers.arcgis.com/javascript/latest/api-reference/esri-layers-FeatureLayer.html) that will be added to the `FloorsSection`. Additionally, if every features have a number attribute `BldgLevel`, the feature filtered depending on the selected floor (See [FloorsSection](./Sections.html)). 12 | - `"Floor pictures"`: this layer needs to be a [`FeatureLayer`](https://developers.arcgis.com/javascript/latest/api-reference/esri-layers-FeatureLayer.html), displaying icons, with the string attributes `url`, `title` and `credit`. It will be added to the `FloorsSection`. When a user cliks on it, a popup is displayed with the information provided through the attributes. Additionally, if every features have a number attribute `BldgLevel`, the feature filtered depending on the selected floor (See [FloorsSection](./Sections.html)). 13 | - `"External pictures"`: this layer needs to be a [`FeatureLayer`](https://developers.arcgis.com/javascript/latest/api-reference/esri-layers-FeatureLayer.html), displaying icons, with the string attributes `url`, `title` and `credit`. It will be added to the `HomeSection`. When a user cliks on it, a popup is displayed with the information provided through the attributes. 14 | 15 | 16 | ### Prefix 17 | 18 | - `"Surroundings:"`: Any layer with name starting with this prefix will be added to the `SurroundingsSection` on the left. A toggle allows user to turn them on or off (off by default). 19 | 20 | 21 | ## Slides 22 | 23 | Slides with the following names have specific roles: 24 | 25 | - `"Overview"`: The slide's camera will be automatically be applied to the view when a user goes to the `HomeSection` 26 | - `"Floor by floor"`: The slide's camera will be automatically be applied to the view when a user goes to the `FloorsSection` 27 | - `"Surroundings"`: The slide's camera will be automatically be applied to the view when a user goes to the `SurroundingsSection` 28 | - In general, a section is looking among the slide's names and any slide matching the section's title will be applied to the view when a user goes to this section 29 | 30 | ### Prefix 31 | 32 | Additionally, this prefix has also a specific meaning: 33 | 34 | - `"Points of Interest:"`: this suffix is used by the `SurroundingsSection` to display different interactive point of interests. When a user clicks on any of the point of interests, the camera is applied to the view. 35 | -------------------------------------------------------------------------------- /docs/Quickstart.md: -------------------------------------------------------------------------------- 1 | # Minimal setup 2 | 3 | ### Prepare a minimal webscene: 4 | 1. Create a new webscene 5 | 1. Add a Building Scene layer and add the suffix `Building:` in front of it's name 6 | 1. Save the webscene, give it a nice title and copy the webscene id. You will need it in a few next steps 7 | 8 | ### Get your own copy of the Building Viewer 9 | 10 | 1. Clone the git repository in your local machine and then go into the folder: 11 | ``` 12 | git clone https://github.com/Esri/building-viewer 13 | cd BSL-Demo-App 14 | ``` 15 | 1. Remove the `.git` repository reference `rm -rf .git` 16 | 1. Install dependency: `npm run install` 17 | 1. Compile the code: `npm run build` 18 | 1. Start the server: `npm run server` 19 | 1. Open your browser and enter the local address `http://localhost:8888/`. You should see now the Building Viewer running: 20 | 21 | 22 | ![The default Building Viewer running](./images/screenshot_1.png) 23 | 24 | 25 | ### Edit the content with your own webscene 26 | 1. Open `src/config.tsx` in your favorite code editor 27 | 2. Delete all the content except the two first obscure lines 28 | 3. Now you need to define 2 parameters in the config to get started: 29 | 1. The `websceneId` of the webscene you created above 30 | ``` 31 | export const websceneId = "YOUR WEBSCENE ID HERE"; 32 | ``` 33 | *Note that you may to also export on which portal this webscene resides if different from the ArcGis's portal: `export const portalUrl = "https://your-portal-url.com";`* 34 | 2. The `sections` you'd like to have in your Building Viewer (see documentation about sections). Let's start with only one section, the home page: 35 | ```typescript 36 | // first import the section: 37 | import HomeSection = require("./sections/HomeSection"); 38 | 39 | // then export the `sections` parameter: 40 | export const sections = [ 41 | new HomeSection({}) 42 | ]; 43 | ``` 44 | 4. Recompile the code (note that the Building Viewer ships with a little util that watch files and recompile for you. Just run `npm run dev` in a differnet terminal window) 45 | 5. You should see your building in the Building Viewer now if you reload your webviewer: 46 | 47 | ![Your building in the Building Viewer](./images/screenshot_2.png) 48 | 49 | ### Let's add content 50 | 51 | The title of the home page is the title of your scene. If you would like to change it, you can simply update the title of the scene. 52 | 53 | 1. First, the view of the building when opening the Building Viewer isn't really Building appealing. Let's change this. 54 | 1. Go back to your webscene 55 | 2. Navigate to a view that please you 56 | 3. Save a slide with name "Overview" 57 | 4. Save your webscene and reload the demo Building Viewer. You should have a better view when you enter the Building Viewer: 58 | 59 | ![The first view](./images/screenshot_3.png) 60 | 61 | 2. Let's add some description on the left side: 62 | 1. Go back to your webscene and go to "Properties" 63 | 2. Add your text in the webscene description 64 | 3. Save the webscene and reload the demo Building Viewer. You should now see your text on the left side: 65 | 66 | ![Description appeared](./images/screenshot_4.png) 67 | 68 | 3. Let's add different point of views for the user to appreciate the building in all corners: 69 | 1. Go back to your webscene and go to "Slides" 70 | 2. Move your view to the location you would like 71 | 3. Create a new slide 72 | 4. Repeat as much as you want, and then save your scene 73 | 5. Reload the demo app, you should see now some points of view: 74 | 75 | ![The point of views](./images/screenshot_5.png) 76 | 77 | 4. Let's add the opening hours: 78 | 1. Go now to the file `src/config.tsx` 79 | 2. Import the necessary classes: 80 | ```typescript 81 | import {Timetable, DayTimetable} from "./widgets/Timetable/Timetable"; 82 | import Collection = require("esri/core/Collection"); 83 | ``` 84 | 3. Add the timetable to the home section, and add all your opening hours for every day: 85 | ```typescript 86 | new HomeSection({ 87 | timetable: new Timetable({ 88 | dates: new Collection([ 89 | new DayTimetable({ 90 | opens: "8:00", 91 | closes: "20:00" 92 | }), 93 | new DayTimetable({ 94 | opens: "8:00", 95 | closes: "20:00" 96 | }), 97 | new DayTimetable({ 98 | opens: "8:00", 99 | closes: "20:00" 100 | }), 101 | new DayTimetable({ 102 | opens: "8:00", 103 | closes: "20:00" 104 | }), 105 | new DayTimetable({ 106 | opens: "8:00", 107 | closes: "20:00" 108 | }), 109 | new DayTimetable({ 110 | opens: "10:00", 111 | closes: "17:00" 112 | }), 113 | new DayTimetable({ 114 | opens: "10:00", 115 | closes: "17:00" 116 | }) 117 | ]) 118 | }) 119 | }) 120 | ``` 121 | 4. Save your file, recompile the typsecrip and reload the app 122 | 5. You should now see the time table on the bottom left of the home page: 123 | 124 | ![The time table](./images/screenshot_6.png) 125 | 126 | 127 | 5. Change the ground colour 128 | 1. The design of the Building Viewer has been set to work well with darker colours. Let's change the background for a dark solid colour 129 | 2. Go back to your webscene and click on "Ground". Choose a good ground colour 130 | 3. Go to the basemap gallery, and check "No basemap". 131 | 4. Save your webscene *(as the initial state)* and reloads the Building Viewer: 132 | 133 | ![Your building on a dark ground](./images/screenshot_7.png) 134 | 135 | 5. Finally, let's add city buildings: 136 | 1. If you have the city as Building Scene Layer, you can add it to your webscene and name it "City model". 137 | 2. Save your webscene and reload the Building Viewer, you should see now the surroundings building: 138 | 139 | ![The city surrounds your building](./images/screenshot_8.png) 140 | 141 | *Note that all others layers or configuration of your webscene will appear in your demo app.* 142 | 143 | --- 144 | 145 | To go beyond and add more section, please read [the sections documentation](./Sections.md). You can always check the [naming conventions](./NameConventions.md) for a quick look at the different layer and slide names. 146 | -------------------------------------------------------------------------------- /docs/Sections.md: -------------------------------------------------------------------------------- 1 | # Sections 2 | 3 | The demo uses 3 different sections, the home section, the floor section and the surroundings section. In the following content, we will go through the initialisation of the different sections and explore how you can create new sections. 4 | 5 | ## General section parameters 6 | 7 | - Every section has a configurable `title` which is then the word appearing in the menu. 8 | - To configure the camera of any section, you can add a slide with the coresponding home's title as the name of the slides. 9 | 10 | ## Home section 11 | 12 | The home section ships with 3 main part: 13 | 14 | - The **description of the building** on the left. By default, the description is taken from the scene's description. As shown in the `Quickstart` guide, you can add your text in the `Properties` pane of the Scene Viewer. However, if you'd like to add more complex content, e.g. involving html or somejavacsript logic, you can always pass a parameter `content` to the `HomeSection` constructor. This parameter is a function that takes in argument the section and return some `VNodes` that will be later added to the left. You are now totally free to design the exact content you would like. 15 | - The **viewpoints**: every slides that you create in your webscene with automatically be added as a viewpoints on the right of the HomeSection if the slide's name is not part of the [reserved names](./NameConventions.md). 16 | - The **building opening hours**: you can pass to your Building Viewer a list of opening hours as follow: 17 | ```typescript 18 | new HomeSection({ 19 | timetable: new Timetable({ 20 | dates: new Collection([ 21 | new DayTimetable({ 22 | opens: "8:00", 23 | closes: "20:00" 24 | }), 25 | new DayTimetable({ 26 | opens: "8:00", 27 | closes: "20:00" 28 | }), 29 | new DayTimetable({ 30 | opens: "8:00", 31 | closes: "20:00" 32 | }), 33 | new DayTimetable({ 34 | opens: "8:00", 35 | closes: "20:00" 36 | }), 37 | new DayTimetable({ 38 | opens: "8:00", 39 | closes: "20:00" 40 | }), 41 | new DayTimetable({ 42 | opens: "10:00", 43 | closes: "17:00" 44 | }), 45 | new DayTimetable({ 46 | opens: "10:00", 47 | closes: "17:00" 48 | }) 49 | ]) 50 | }) 51 | }) 52 | ``` 53 | 54 | ## Floor section 55 | 56 | The floor section will display a floor picker on the right that allows the user to discover the different level's of the building. There is two way to initialise the floor section. You can either pass the lowest floor number and the hightest floor number as follow: 57 | 58 | ```typescript 59 | new FloorsSection({ 60 | minFloor: 0, 61 | maxFloor: 2 62 | }) 63 | ``` 64 | 65 | which will allow the user to go through floors 0 to 2. Or you can pass every floor with the content it needs to display on the left of the building when a user select one: 66 | 67 | ```typescript 68 | new FloorsSection({ 69 | floors: new Collection([ 70 | ..., 71 | new Floor({ 72 | title: "The name of the floor", 73 | subtitle: "A subtitle", 74 | floor: 0, 75 | content: () => (
Some html content
) 76 | }), 77 | ... 78 | ]) 79 | }) 80 | ``` 81 | 82 | This uses [TSX](https://www.typescriptlang.org/docs/handbook/jsx.html) to render the content. Be sure to include the `tsx` function [from the ArcGIS for Javascript API](https://developers.arcgis.com/javascript/latest/api-reference/esri-widgets-support-widget.html#tsx) to compile this code. 83 | 84 | 85 | ## Surroundings section 86 | 87 | 88 | The surroundings section display toggles for extra layers you can setup in your webscene or point of view for different building in your surrounding layer, using your webscene's slides (See [naming concention](./NamingConvention.md)). 89 | 90 | It does not take any parameter to be initialised: 91 | 92 | ```typescript 93 | new SurroundingsSection({}) 94 | ``` 95 | 96 | - To add toggle for extra layers, just add `"Surroundings:"` in front of their layer's name in your webscene. 97 | - To add toggle for point of view of building in your "surroundings" layer, just add slides with `"Points of Interest:"` in their title. 98 | 99 | 100 | ## Create your own section 101 | 102 | The building viewer as been designed so that you can easily extend it. Every section share a common base class `Section` which defines the minimal structure for it to be displayed. If you want to create your own section, you can simple extend this class and define the `title`, give it a unique `id` and define what goes on the right side by delcaring `, and what goes on the left by define `render`. 103 | 104 | As an example: 105 | 106 | ``` 107 | @subclass() 108 | class MySection extends declared(Section) { 109 | @property() 110 | title = "My section" 111 | 112 | @property() 113 | id = "my-section" 114 | 115 | render() { 116 | return (
); 117 | } 118 | 119 | paneRight() { 120 | return (
); 121 | } 122 | ``` 123 | 124 | You can of course create a complex widget here. This is following the ArcGIS for Javascript API's [widget convention and structure](https://developers.arcgis.com/javascript/latest/api-reference/esri-widgets-Widget.html). Be sure to check their [guide first](https://developers.arcgis.com/javascript/latest/guide/custom-widget/index.html). 125 | -------------------------------------------------------------------------------- /docs/images/screenshot_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/building-viewer/4b677fffb43b566e0f001e8b9fd04115ad078b9d/docs/images/screenshot_1.png -------------------------------------------------------------------------------- /docs/images/screenshot_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/building-viewer/4b677fffb43b566e0f001e8b9fd04115ad078b9d/docs/images/screenshot_2.png -------------------------------------------------------------------------------- /docs/images/screenshot_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/building-viewer/4b677fffb43b566e0f001e8b9fd04115ad078b9d/docs/images/screenshot_3.png -------------------------------------------------------------------------------- /docs/images/screenshot_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/building-viewer/4b677fffb43b566e0f001e8b9fd04115ad078b9d/docs/images/screenshot_4.png -------------------------------------------------------------------------------- /docs/images/screenshot_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/building-viewer/4b677fffb43b566e0f001e8b9fd04115ad078b9d/docs/images/screenshot_5.png -------------------------------------------------------------------------------- /docs/images/screenshot_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/building-viewer/4b677fffb43b566e0f001e8b9fd04115ad078b9d/docs/images/screenshot_6.png -------------------------------------------------------------------------------- /docs/images/screenshot_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/building-viewer/4b677fffb43b566e0f001e8b9fd04115ad078b9d/docs/images/screenshot_7.png -------------------------------------------------------------------------------- /docs/images/screenshot_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/building-viewer/4b677fffb43b566e0f001e8b9fd04115ad078b9d/docs/images/screenshot_8.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Building Viewer 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 25 | 26 | 27 | 28 | 29 |
30 |
31 |
32 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Apache License - 2.0 2 | 3 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 4 | 5 | 1. Definitions. 6 | 7 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. 8 | 9 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 10 | 11 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control 12 | with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management 13 | of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial 14 | ownership of such entity. 15 | 16 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. 17 | 18 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, 19 | and configuration files. 20 | 21 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to 22 | compiled object code, generated documentation, and conversions to other media types. 23 | 24 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice 25 | that is included in or attached to the work (an example is provided in the Appendix below). 26 | 27 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the 28 | editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes 29 | of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, 30 | the Work and Derivative Works thereof. 31 | 32 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work 33 | or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual 34 | or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of 35 | electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on 36 | electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for 37 | the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing 38 | by the copyright owner as "Not a Contribution." 39 | 40 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and 41 | subsequently incorporated within the Work. 42 | 43 | 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, 44 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, 45 | publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 46 | 47 | 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, 48 | non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, 49 | sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are 50 | necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was 51 | submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work 52 | or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You 53 | under this License for that Work shall terminate as of the date such litigation is filed. 54 | 55 | 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, 56 | and in Source or Object form, provided that You meet the following conditions: 57 | 58 | 1. You must give any other recipients of the Work or Derivative Works a copy of this License; and 59 | 60 | 2. You must cause any modified files to carry prominent notices stating that You changed the files; and 61 | 62 | 3. You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices 63 | from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and 64 | 65 | 4. If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a 66 | readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the 67 | Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the 68 | Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever 69 | such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. 70 | You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, 71 | provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to 72 | Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your 73 | modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with 74 | the conditions stated in this License. 75 | 76 | 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You 77 | to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, 78 | nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 79 | 80 | 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except 81 | as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 82 | 83 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides 84 | its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, 85 | any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for 86 | determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under 87 | this License. 88 | 89 | 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required 90 | by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, 91 | including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the 92 | use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or 93 | any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 94 | 95 | 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a 96 | fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting 97 | such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree 98 | to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your 99 | accepting any such warranty or additional liability. 100 | 101 | END OF TERMS AND CONDITIONS 102 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "building-viewer", 3 | "version": "1.0.0", 4 | "description": "This application demonstrates the use of various Building Scene Layer related API", 5 | "repository": { 6 | "type": "git", 7 | "url": "git@github.com:esri/building-viewer.git" 8 | }, 9 | "scripts": { 10 | "dev": "tsc -w & sass --watch src:src", 11 | "lint": "tslint './src/*.ts'", 12 | "build": "tsc & sass src:src", 13 | "dist": "grunt dist", 14 | "server": "grunt" 15 | }, 16 | "keywords": [ 17 | "esri", 18 | "typescript" 19 | ], 20 | "author": "Yannik Messerli ", 21 | "license": "Apache-2.0", 22 | "devDependencies": { 23 | "grunt": "^1.3.0", 24 | "grunt-contrib-concat": "*", 25 | "grunt-contrib-connect": "*", 26 | "grunt-contrib-copy": "*", 27 | "grunt-contrib-htmlmin": "*", 28 | "grunt-contrib-uglify": "*", 29 | "grunt-contrib-watch": "*", 30 | "grunt-dojo": "^1.1.2", 31 | "grunt-include-source": "^1.1.0", 32 | "grunt-run": "^0.8.1", 33 | "grunt-stripcomments": "^0.7.2", 34 | "js-yaml": ">=3.13.1", 35 | "tslint": "^4.2.0", 36 | "tslib": "~2.4.0", 37 | "sass": "^1.49.10" 38 | }, 39 | "dependencies": { 40 | "@types/dojo": "1.9.41", 41 | "typescript": "4.2.4" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/css/main.scss: -------------------------------------------------------------------------------- 1 | @import "./variables.scss"; 2 | @import "./mixins.scss"; 3 | 4 | /*************** 5 | Base 6 | ****************/ 7 | 8 | html, 9 | body, 10 | #mainViewDiv, #appDiv, #popup { 11 | padding: 0; 12 | margin: 0; 13 | height: 100%; 14 | width: 100%; 15 | } 16 | 17 | #appDiv { 18 | top: 0; 19 | pointer-events: none; 20 | position: absolute; 21 | overflow: hidden; 22 | } 23 | 24 | #mainViewDiv { 25 | position: absolute; 26 | bottom: 0; 27 | left: 0; 28 | @include transition(all 0.8s); 29 | } 30 | 31 | /*************** 32 | Typography 33 | ****************/ 34 | 35 | #appDiv { 36 | color: $primaryColor; 37 | font-size: $generalFontSize; 38 | line-height: $generalFontSize + 7px; 39 | text-shadow: 1px 1px 5px rgba(70,70,70, 1); 40 | 41 | a { 42 | color: $primaryColor; 43 | text-decoration: underline; 44 | } 45 | 46 | h1, h2, h3, h4, h5 { 47 | // font-family: "HelveticaNeue-CondensedBlack", "Helvetica Neue"; 48 | font-family: 'Roboto Condensed', sans-serif; 49 | text-transform: uppercase; 50 | font-stretch: "condensed"; 51 | pointer-events: all; 52 | font-weight: 900; 53 | text-shadow: 0 0 5px rgba(70,70,70, 1); 54 | 55 | &.slash-title { 56 | text-transform: none; 57 | font-family: "Avenir Next"; 58 | font-stretch: "normal"; 59 | font-weight: normal; 60 | 61 | &::before { 62 | content: "/"; 63 | margin-right: 10px; 64 | } 65 | } 66 | 67 | &.inline { 68 | margin-right: 10px; 69 | } 70 | } 71 | 72 | h2.slash-title { 73 | margin-bottom: 50px; 74 | margin-top: 30px; 75 | } 76 | 77 | h5.inline { 78 | font-family:'Roboto Condensed', sans-serif; 79 | font-size: 15px; 80 | } 81 | 82 | .inline { 83 | display: inline; 84 | } 85 | 86 | p { 87 | pointer-events: all; 88 | } 89 | 90 | h1 { 91 | // font-family: "HelveticaNeue-CondensedBlack", "Helvetica Neue"; 92 | font-family:'Roboto Condensed', sans-serif; 93 | font-stretch: "condensed"; 94 | font-weight: 900; 95 | font-size: $bigTitleFontSize; 96 | line-height: $bigTitleFontSize; 97 | margin-bottom: 30px; 98 | } 99 | } 100 | 101 | @media (max-width: 1400px) { 102 | #appDiv { 103 | font-size: 12px; 104 | line-height: 17px; 105 | 106 | .active .viewpoints { 107 | margin-top: 50px; 108 | 109 | h2.slash-title { 110 | margin-bottom: -5px; 111 | } 112 | 113 | .viewpoint { 114 | font-size: 21px; 115 | } 116 | } 117 | #surroundings h1 { 118 | font-size: 30px; 119 | margin-bottom: 10px; 120 | } 121 | 122 | #surroundings .slash-title.width-toggle { 123 | font-size: 20px; 124 | } 125 | 126 | #surroundings .content { 127 | font-size: 17px; 128 | } 129 | 130 | #surroundings .content { 131 | line-height: 30px; 132 | } 133 | 134 | #surroundings .content svg { 135 | width: 13px; 136 | vertical-align: -2px; 137 | } 138 | 139 | #surroundings .element a { 140 | margin-left: 8px; 141 | } 142 | 143 | #menu { 144 | font-size: 18px; 145 | 146 | .slash { 147 | margin-left: 20px; 148 | margin-right: 15px; 149 | } 150 | } 151 | 152 | .side-container { 153 | max-width: 290px; 154 | } 155 | 156 | h1 { 157 | font-size: 50px; 158 | line-height: 50px; 159 | } 160 | 161 | h2.slash-title { 162 | margin-top: 10px; 163 | margin-bottom: 10px; 164 | font-size: 25px; 165 | } 166 | 167 | .timetable { 168 | margin-top: 20px; 169 | 170 | .daytime { 171 | height: 20px; 172 | 173 | h2 { 174 | font-size: 20px; 175 | margin-top: -4px; 176 | } 177 | 178 | h3 { 179 | font-size: 15px; 180 | margin-bottom: 3px; 181 | } 182 | } 183 | } 184 | 185 | #surroundings .content { 186 | margin-top: 0px; 187 | } 188 | 189 | .active .floor-selector .level:first-child { 190 | margin-top: 0; 191 | } 192 | 193 | #floors { 194 | h1 { 195 | font-size: 40px; 196 | line-height: 40px; 197 | margin-left: 70px; 198 | width: 220px; 199 | 200 | &.number { 201 | font-size: 100px; 202 | margin-top: 20px; 203 | margin-left: 0; 204 | } 205 | } 206 | 207 | h3.subtitle { 208 | font-size: 25px; 209 | margin-left: 75px; 210 | letter-spacing: 4px; 211 | } 212 | 213 | .level { 214 | margin-top: 33px; 215 | margin-left: 12px; 216 | } 217 | } 218 | 219 | .side-container.left .pane { 220 | width: 290px; 221 | } 222 | } 223 | } 224 | 225 | /*************** 226 | Layout 227 | ****************/ 228 | 229 | .side-container { 230 | top: 0; 231 | position: absolute; 232 | max-width: 390px; 233 | margin: 50px; 234 | } 235 | .side-container.left { 236 | left: 0; 237 | } 238 | 239 | .side-container.right { 240 | right: 0; 241 | margin-top: 20px; 242 | text-align: right; 243 | } 244 | 245 | #menu { 246 | pointer-events: all; 247 | text-align: center; 248 | position: absolute; 249 | left: 0; 250 | position: absolute; 251 | margin-left: auto; 252 | margin-right: auto; 253 | width: 100%; 254 | text-align: center; 255 | display: block; 256 | font-size: $menuItemFontSize; 257 | font-family: "Avenir Next"; 258 | font-stretch: "normal"; 259 | font-weight: medium; 260 | top: $menuTopPosition; 261 | cursor: pointer; 262 | 263 | .slash { 264 | margin-left: $menuSlashMarginLeft; 265 | margin-right: $menuSlashMarginRight; 266 | } 267 | 268 | a { 269 | text-decoration: none; 270 | color: $menuItemColor; 271 | cursor: pointer; 272 | @include transition(font-size 0.3s); 273 | 274 | &.active { 275 | font-size: $menuItemActiveFontSize; 276 | color: $menuItemActiveColor; 277 | 278 | &:hover { 279 | color: $menuItemActiveColor; 280 | } 281 | } 282 | 283 | &:hover { 284 | font-size: $menuItemOverFontSize; 285 | color: $menuItemOverColor; 286 | } 287 | } 288 | 289 | } 290 | 291 | .esri-attribution { 292 | display: none; 293 | } 294 | -------------------------------------------------------------------------------- /src/css/mixins.scss: -------------------------------------------------------------------------------- 1 | /*************** 2 | Mixins 3 | ****************/ 4 | 5 | @mixin transition($transition...) { 6 | -moz-transition: $transition; 7 | -o-transition: $transition; 8 | -webkit-transition: $transition; 9 | transition: $transition; 10 | } 11 | 12 | @mixin transition-property($property...) { 13 | -moz-transition-property: $property; 14 | -o-transition-property: $property; 15 | -webkit-transition-property: $property; 16 | transition-property: $property; 17 | } 18 | @mixin transition-duration($duration...) { 19 | -moz-transition-property: $duration; 20 | -o-transition-property: $duration; 21 | -webkit-transition-property: $duration; 22 | transition-property: $duration; 23 | } 24 | @mixin transition-timing-function($timing...) { 25 | -moz-transition-timing-function: $timing; 26 | -o-transition-timing-function: $timing; 27 | -webkit-transition-timing-function: $timing; 28 | transition-timing-function: $timing; 29 | } 30 | @mixin transition-delay($delay...) { 31 | -moz-transition-delay: $delay; 32 | -o-transition-delay: $delay; 33 | -webkit-transition-delay: $delay; 34 | transition-delay: $delay; 35 | } 36 | -------------------------------------------------------------------------------- /src/css/variables.scss: -------------------------------------------------------------------------------- 1 | /*************** 2 | Colors 3 | ****************/ 4 | 5 | // Global: 6 | $primaryColor: #fff; 7 | $secondaryColor: #a3a3a3; 8 | $orange: #F6A803; 9 | 10 | // Menu: 11 | $menuItemColor: $primaryColor; 12 | $menuItemActiveColor: $orange; 13 | $menuItemOverColor: $primaryColor; 14 | 15 | // Viewpoints: 16 | $viewpointItemColor: $primaryColor; 17 | $viewpointItemActiveColor: $orange; 18 | $viewpointItemOverColor: $primaryColor; 19 | 20 | // Floors 21 | 22 | // Surroundings 23 | 24 | 25 | /*************** 26 | Font sizing 27 | ****************/ 28 | 29 | $generalFontSize: 15px; 30 | $bigTitleFontSize: 5 * $generalFontSize + 10px; 31 | 32 | // Menu 33 | $menuItemFontSize: 23px; 34 | $menuItemActiveFontSize: 23px; 35 | $menuItemOverFontSize: 26px; 36 | 37 | // Viewpoints 38 | $viewpointItemFontSize: 25px; 39 | $viewpointItemActiveFontSize: 40px; 40 | $viewpointItemOverFontSize: 40px; 41 | 42 | // Floors 43 | $floorSelectorLevelFontSize: 25px; 44 | $floorSelectorLevelOverFontSize: 60px; 45 | $floorSelectorLevelActiveFontSize: 90px; 46 | 47 | /*************** 48 | Sizing 49 | ****************/ 50 | 51 | // Menu 52 | $menuTopPosition: 25px; 53 | $menuSlashMarginLeft: $menuItemFontSize + 2px; 54 | $menuSlashMarginRight: $menuItemFontSize - 3px; 55 | -------------------------------------------------------------------------------- /src/js/AppState.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Esri 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | import { subclass, property } from "esri/core/accessorSupport/decorators"; 18 | import Accessor from "esri/core/Accessor"; 19 | import SceneView from "esri/views/SceneView"; 20 | import BuildingVisualisation from "./support/BuildingVisualisation"; 21 | import PopupInfo from "./widgets/Popup/PopupInfo"; 22 | 23 | @subclass("AppState") 24 | class AppState extends Accessor { 25 | @property() 26 | pageLocation: string; 27 | 28 | @property() 29 | floorNumber = 0; 30 | 31 | @property() 32 | view: SceneView; 33 | 34 | @property() 35 | buildingLayer: BuildingVisualisation; 36 | 37 | @property() 38 | popupInfo: PopupInfo; 39 | } 40 | 41 | export = AppState; 42 | -------------------------------------------------------------------------------- /src/js/BuildingViewer.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Esri 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | import { subclass, property } from "esri/core/accessorSupport/decorators"; 18 | import { tsx } from "esri/widgets/support/widget"; 19 | 20 | // esri 21 | import Sections from "./sections/Sections"; 22 | import SceneView from "esri/views/SceneView"; 23 | import Widget from "esri/widgets/Widget"; 24 | import * as promiseUtils from "esri/core/promiseUtils"; 25 | import Camera from "esri/Camera"; 26 | import SceneLayer from "esri/layers/SceneLayer"; 27 | import BuildingSceneLayer from "esri/layers/BuildingSceneLayer"; 28 | import WebScene from "esri/WebScene"; 29 | 30 | // BuildingViewer 31 | import Section from "./sections/Section"; 32 | import BuildingVisualisation from "./support/BuildingVisualisation"; 33 | import SurroundingsVisualisation from "./support/SurroundingsVisualisation"; 34 | import AppState from "./AppState"; 35 | import * as appUtils from "./support/appUtils"; 36 | import Popup from "./widgets/Popup/Popup"; 37 | 38 | type SectionSublcass = Pick; 39 | 40 | interface BuildingViewerCtorArgs { 41 | sections: Pick[]; 42 | mapContainer: string; 43 | websceneId: string; 44 | portalUrl?: string; 45 | floorMapping?: (originalFloor: number) => number; 46 | extraQuery?: string; 47 | } 48 | 49 | @subclass("webSceneViewer.widgets.LayersLoading.LayersLoadingProgressBar") 50 | class BuildingViewer extends Widget { 51 | //-------------------------------------------------------------------------- 52 | // 53 | // Properties 54 | // 55 | //-------------------------------------------------------------------------- 56 | 57 | @property({ aliasOf: "appState.view"}) 58 | view: SceneView; 59 | 60 | @property({ aliasOf: "sections.activeSection"}) 61 | activeSection: SectionSublcass | string | number; 62 | 63 | @property() 64 | sections: Sections; 65 | 66 | @property() 67 | appState = new AppState(); 68 | 69 | @property() 70 | websceneId: string; 71 | 72 | @property() 73 | extraQuery: string; 74 | 75 | @property() 76 | portalUrl: string; 77 | 78 | //-------------------------------------------------------------------------- 79 | // 80 | // Variables: 81 | // 82 | //-------------------------------------------------------------------------- 83 | 84 | @property({ aliasOf: "appState.buildingLayer"}) 85 | buildingLayer: BuildingVisualisation; 86 | 87 | @property({ aliasOf: "appState.surroundingsLayer"}) 88 | surroundingsLayer: SurroundingsVisualisation; 89 | 90 | private firstRendering: boolean = true; 91 | 92 | private rawSections: Pick[]; 93 | 94 | //-------------------------------------------------------------------------- 95 | // 96 | // Life circle 97 | // 98 | //-------------------------------------------------------------------------- 99 | 100 | constructor(args: BuildingViewerCtorArgs) { 101 | super(args as any); 102 | 103 | this.view = appUtils.createViewFromWebScene({websceneId: args.websceneId, mapContainer: args.mapContainer, portalUrl: args.portalUrl}); 104 | 105 | if (args.floorMapping) { 106 | this.floorMapping = args.floorMapping.bind(this); 107 | } 108 | } 109 | 110 | normalizeCtorArgs(args: BuildingViewerCtorArgs) { 111 | this.rawSections = args.sections; 112 | delete args["sections"]; 113 | 114 | return args; 115 | } 116 | 117 | initialize() { 118 | this.sections = new Sections(this.rawSections, this.appState); 119 | 120 | (this.view.map as WebScene).when(() => { 121 | // Save the initial layers: 122 | promiseUtils 123 | .eachAlways(this.view.map.layers.map((l) => this.appState.view.whenLayerView(l))) 124 | .then(() => { 125 | 126 | /////////////////////////////////// 127 | // Main building to present: 128 | const BSL = this.appState.view.map.layers.find(layer => layer.title.indexOf(appUtils.MAIN_LAYER_PREFIX) > -1); 129 | 130 | if (!BSL) { 131 | throw new Error("Cannot find the main BuildingSceneLayer (" + appUtils.MAIN_LAYER_PREFIX + ") in the webscene " + this.websceneId); 132 | } 133 | 134 | const visualisationArgs: any = { 135 | appState: this.appState, 136 | layer: BSL as BuildingSceneLayer 137 | }; 138 | 139 | if (this.floorMapping) { 140 | visualisationArgs.floorMapping = this.floorMapping; 141 | } 142 | 143 | if (this.extraQuery) { 144 | visualisationArgs.extraQuery = this.extraQuery; 145 | } 146 | 147 | this.buildingLayer = new BuildingVisualisation(visualisationArgs); 148 | 149 | /////////////////////////////////// 150 | // Optional surrounding's layer: 151 | const surroundingsLayer = this.appState.view.map.layers.find(layer => layer.title.toLowerCase().indexOf(appUtils.CITY_LAYER_PREFIX.toLowerCase()) > -1) as SceneLayer; 152 | if (surroundingsLayer) { 153 | this.surroundingsLayer = new SurroundingsVisualisation({ 154 | layer: surroundingsLayer, 155 | appState: this.appState 156 | }); 157 | } 158 | }); 159 | 160 | /////////////////////////////////// 161 | // Setup camera: 162 | this.sections.forEach((section) => { 163 | const slide = (this.view.map as WebScene).presentation.slides.find((slide) => slide.title.text === section.title); 164 | if (slide) { 165 | section.camera = slide.viewpoint.camera; 166 | (this.view.map as WebScene).presentation.slides.remove(slide); 167 | } 168 | else { 169 | console.error("Could not find a slide for section " + section.title); 170 | } 171 | }); 172 | }); 173 | 174 | this.view.when(() => { 175 | // Debug: 176 | window["view"] = this.view; 177 | window["appState"] = this.appState; 178 | 179 | // Active first section: 180 | if (this.sections.length > 0) { 181 | this.sections.activateSection(this.sections.getItemAt(0).id); 182 | } 183 | }); 184 | 185 | this.watch("activeSection", (activeSection) => { 186 | this.firstRendering = true; 187 | this.renderNow(); 188 | 189 | setTimeout(() => { 190 | this.firstRendering = false; 191 | this.renderNow(); 192 | }, 10) 193 | }); 194 | } 195 | 196 | render() { 197 | return (
198 |
{this.sections.paneLeft(this.firstRendering)}
199 | 200 |
{this.sections.paneRight(this.firstRendering)}
201 |
); 202 | } 203 | 204 | postInitialize() { 205 | 206 | this.own(this.sections.on("go-to", (camera: Camera) => { 207 | this.view.goTo(camera); 208 | })); 209 | 210 | new Popup({ appState: this.appState, container: "popup"}); 211 | } 212 | 213 | floorMapping(num: number) { return num; } 214 | } 215 | 216 | export = BuildingViewer; 217 | -------------------------------------------------------------------------------- /src/js/config.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Esri 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | import { tsx } from "esri/widgets/support/widget"; 18 | import HomeSection from "./sections/HomeSection"; 19 | import { FloorsSection, Floor } from "./sections/FloorsSection"; 20 | import SurroundingsSection from "./sections/SurroundingsSection"; 21 | import Collection from "esri/core/Collection"; 22 | import {Timetable, DayTimetable} from "./widgets/Timetable/Timetable"; 23 | 24 | export const portalUrl = "https://zurich.maps.arcgis.com"; 25 | 26 | export const websceneId = "543648a92446497db8a92c06ce1ad0b1"; 27 | 28 | export const sections = [ 29 | // Check the different files 30 | // to adapt to your need 31 | // or create a new section by 32 | // implement a subclass from `Section` 33 | 34 | // The about Turangua section: 35 | new HomeSection({ 36 | content: (that: any) => (

Tūranga is a library in Central Christchurch and the main library of Christchurch City Libraries, New Zealand. It is the largest library in the South Island and the third-biggest in New Zealand. The previous Christchurch Central Library opened in 1982 on the corner of Oxford Terrace and Gloucester Street but was closed after the February 2011 Christchurch earthquake and demolished in 2014 to make way for the Convention Centre Precinct.

), 37 | timetable: new Timetable({ 38 | dates: new Collection([ 39 | new DayTimetable({ 40 | opens: "8:00", 41 | closes: "20:00" 42 | }), 43 | new DayTimetable({ 44 | opens: "8:00", 45 | closes: "20:00" 46 | }), 47 | new DayTimetable({ 48 | opens: "8:00", 49 | closes: "20:00" 50 | }), 51 | new DayTimetable({ 52 | opens: "8:00", 53 | closes: "20:00" 54 | }), 55 | new DayTimetable({ 56 | opens: "8:00", 57 | closes: "20:00" 58 | }), 59 | new DayTimetable({ 60 | opens: "10:00", 61 | closes: "17:00" 62 | }), 63 | new DayTimetable({ 64 | opens: "10:00", 65 | closes: "17:00" 66 | }) 67 | ]) 68 | }) 69 | }), 70 | // The different floors for Turanga: 71 | new FloorsSection({ 72 | floors: new Collection([ 73 | new Floor({ 74 | title: "He Hononga", 75 | subtitle: "connection", 76 | audio: "https://my.christchurchcitylibraries.com/wp-content/uploads/sites/5/2019/01/He-Hononga.mp3", 77 | floor: 0, 78 | content: (that: any) => (

Open an hour earlier than the rest of the building on weekdays, He Hononga | Connection, Ground Level is the place to return library items, collect holds, browse magazines, DVDs and new arrivals, visit the café or interact with the Discovery Wall.

) 79 | }), 80 | new Floor({ 81 | title: "Hapori", 82 | subtitle: "community", 83 | audio: "https://my.christchurchcitylibraries.com/wp-content/uploads/sites/5/2019/01/Hapori.mp3", 84 | floor: 1, 85 | content: (that: any) => (

It offers experiences geared towards a wide cross-section of our community. Grab a hot drink at the espresso bar, attend an event in our community arena, or help the kids explore the play and craft areas and children’s resources. It’s also a great place for young adults to hang out, play videogames, try out VR or get some study done.

) 86 | }), 87 | new Floor({ 88 | title: "Tuakiri", 89 | subtitle: "identity", 90 | audio: "https://my.christchurchcitylibraries.com/wp-content/uploads/sites/5/2019/01/Tuakiri.mp3", 91 | floor: 2, 92 | content: (that: any) => (

Find resources and services to help you develop your knowledge about your own identity, your ancestors, your whakapapa and also about the place that they called home – its land and buildings.

) 93 | }), 94 | new Floor({ 95 | title: "Tūhuratanga", 96 | subtitle: "discovery", 97 | audio: "https://my.christchurchcitylibraries.com/wp-content/uploads/sites/5/2019/01/T%C5%ABhuratanga.mp3", 98 | floor: 3, 99 | content: (that: any) => (

Explore the nonfiction collection with thousands of books on a huge range of subjects. Get help with print and online resources for research or recreation. Use the public internet computers or, for those who want a low-key space to read or study, there is a separate room called ‘The Quiet Place’. Study, research or browse for some recreational reading.

) 100 | }), 101 | new Floor({ 102 | title: "Auahatanga", 103 | subtitle: "creativity", 104 | audio: "https://my.christchurchcitylibraries.com/wp-content/uploads/sites/5/2019/01/Auahatanga.mp3", 105 | floor: 4, 106 | content: (that: any) => (

Browse the World Languages, Music and Fiction collections, including Biographies and Graphic Novels. Visit the two roof gardens with great views across the city. Explore your creativity in the Production Studio using creative technology such as 3D printers and sewing machines. Create and edit music and video using the Audio/Video Studio, or take a class in the Computer Labs with a great range of software available.

) 107 | }) 108 | ]) 109 | }), 110 | // Surroundings: 111 | new SurroundingsSection({}) 112 | ]; 113 | 114 | export const floorMapping = (originalFloor: number) => { 115 | let floor = originalFloor + 1; 116 | if (floor >= 3) { 117 | floor += 1; 118 | } 119 | 120 | return floor; 121 | } 122 | 123 | export const extraQuery = " AND (Category <> 'Generic Models' OR OBJECTID_1 = 2) AND Category <> 'Walls' AND Category <> 'Roofs' AND Category <> 'Curtain Wall Mullions' AND Category <> 'Curtain Panels'"; 124 | -------------------------------------------------------------------------------- /src/js/sections/FloorsSection.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Esri 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | import { subclass, property } from "esri/core/accessorSupport/decorators"; 18 | import { tsx } from "esri/widgets/support/widget"; 19 | import Section from "./Section"; 20 | import Collection from "esri/core/Collection"; 21 | import Widget from "esri/widgets/Widget"; 22 | import FloorSelector from "../widgets/FloorSelector/FloorSelector"; 23 | import * as watchUtils from "esri/core/watchUtils"; 24 | import FeatureLayer from "esri/layers/FeatureLayer"; 25 | import Legend from "esri/widgets/Legend"; 26 | import PopupInfo from "../widgets/Popup/PopupInfo"; 27 | import * as appUtils from "../support/appUtils"; 28 | import Handles from "esri/core/Handles"; 29 | import AppState from "../AppState"; 30 | 31 | @subclass("legendWrapper") 32 | class LegendWrapper extends Widget { 33 | @property() 34 | hide: boolean = true; 35 | 36 | @property({ constructOnly: true }) 37 | appState: AppState; 38 | 39 | @property() 40 | legend: Legend; 41 | 42 | constructor(args: { appState: AppState }, container: string) { 43 | super(args as any); 44 | } 45 | 46 | postInitialize() { 47 | this.legend = new Legend({ 48 | view: this.appState.view, 49 | layerInfos: [] 50 | }); 51 | } 52 | 53 | render() { 54 | return (
55 | {this.legend.render()} 56 |
); 57 | } 58 | } 59 | 60 | 61 | @subclass("playButton") 62 | class PlayButton extends Widget { 63 | @property() 64 | playing: boolean = false; 65 | 66 | @property() 67 | audioSrc: string; 68 | 69 | @property({dependsOn: ["audioSrc"], readOnly: true }) 70 | get audio() { 71 | return new Audio(this.audioSrc); 72 | } 73 | 74 | postInitialize() { 75 | this.watch("audio", audio => { 76 | audio.addEventListener("ended", () => { 77 | audio.currentTime = 0; 78 | this.playing = false; 79 | }); 80 | }); 81 | } 82 | 83 | render() { 84 | const dynamicCss = { 85 | "playing": this.playing 86 | }; 87 | 88 | return ( 89 | 94 | ); 95 | } 96 | 97 | onClick(event: Event) { 98 | if (this.playing) { 99 | this.playing = false; 100 | this.audio.pause(); 101 | } 102 | else { 103 | this.audio.play(); 104 | this.playing = true; 105 | } 106 | } 107 | } 108 | 109 | interface FloorCtorArgs { 110 | title: string; 111 | subtitle: string; 112 | content: (that: Floor) => any; 113 | floor: number; 114 | audio?: string; 115 | } 116 | 117 | interface FloorsSectionCtorArgs { 118 | floors?: Collection; 119 | } 120 | 121 | interface FloorsSectionCtorArgs2 { 122 | minFloor: number; 123 | maxFloor: number; 124 | } 125 | 126 | @subclass("Floor") 127 | export class Floor extends Widget { 128 | @property() 129 | title: string; 130 | 131 | @property() 132 | content: (that: this) => any; 133 | 134 | @property() 135 | subtitle: string; 136 | 137 | @property() 138 | floor = 1; 139 | 140 | @property({aliasOf: "playButton.audioSrc"}) 141 | audio: string; 142 | 143 | @property() 144 | playButton = new PlayButton(); 145 | 146 | render() { 147 | const audio = this.audio ? (

Listen to the name of this floor {this.playButton.render()}

) : null; 148 | return (
149 | {this.content(this)} 150 | {audio} 151 |
); 152 | } 153 | 154 | constructor(args: FloorCtorArgs) { 155 | super(args as any); 156 | } 157 | 158 | activate() { 159 | // put audio back to 0 160 | this.playButton.audio.currentTime = 0; 161 | } 162 | } 163 | 164 | @subclass("sections/FloorsSection") 165 | export class FloorsSection extends Section { 166 | @property() 167 | title = "Floor by floor"; 168 | 169 | @property() 170 | id = "floors"; 171 | 172 | @property({ aliasOf: "appState.floorNumber"}) 173 | selectedFloor: number; 174 | 175 | private oldDate: Date; 176 | 177 | @property() 178 | previousSelectedFloor: number; 179 | 180 | @property() 181 | floorSelector: FloorSelector; 182 | 183 | @property() 184 | legendWrapper: LegendWrapper; 185 | 186 | @property() 187 | layer: FeatureLayer; 188 | 189 | @property({constructOnly: true }) 190 | layerNameForInfoPoint = appUtils.FLOOR_POINTS_LAYER_PREFIX; 191 | 192 | @property({constructOnly: true }) 193 | layerNameForPicturePoint = appUtils.INTERNAL_INFOPOINTS_LAYER_PREFIX; 194 | 195 | @property() 196 | picturePointsLayer: FeatureLayer; 197 | 198 | @property() 199 | minFloor: number; 200 | 201 | @property() 202 | maxFloor: number; 203 | 204 | private handles = new Handles(); 205 | 206 | @property({constructOnly: true}) 207 | floors: Collection; 208 | 209 | render() { 210 | const currentLevel = this.floors ? this.floors.getItemAt(this.selectedFloor) : null; 211 | const selectedFloor = this.selectedFloor === 0 ? "G" : this.selectedFloor; 212 | const title = currentLevel ? this.selectedFloor === 0 ? (

{currentLevel.title}

) : (

{currentLevel.title}

) : null; 213 | return currentLevel ? (
214 |
floor
215 |

{selectedFloor}

216 | {title} 217 |

[{currentLevel.subtitle}]

218 |
{currentLevel.render()}
219 |
) : null; 220 | } 221 | 222 | paneRight() { 223 | const floorSelector = this.floorSelector ? this.floorSelector.render() : null; 224 | return (
{floorSelector}
); 225 | } 226 | 227 | constructor(args: FloorsSectionCtorArgs | FloorsSectionCtorArgs2) { 228 | super(args as any); 229 | } 230 | 231 | postInitialize() { 232 | watchUtils.whenOnce(this, "appState", () => { 233 | this.legendWrapper = new LegendWrapper({ 234 | appState: this.appState 235 | }, "floorLegend"); 236 | 237 | const floorSelectorCtorArgs = this.minFloor != null && this.maxFloor != null ? { 238 | appState: this.appState, 239 | minFloor: this.minFloor, 240 | maxFloor: this.maxFloor 241 | } : { 242 | appState: this.appState 243 | } 244 | 245 | this.floorSelector = new FloorSelector(floorSelectorCtorArgs); 246 | 247 | watchUtils.on(this, "appState.view.map.layers", "change", this.getExtraInfoLayers.bind(this)); 248 | 249 | watchUtils.init(this, "selectedFloor", (selectedFloor) => { 250 | if (this.floors) { 251 | this.floors.getItemAt(selectedFloor).activate(); 252 | } 253 | 254 | // filter the picture and infoLayer: 255 | if (this.layer) { 256 | this.layer.definitionExpression = "level_id = " + selectedFloor; 257 | } 258 | 259 | if (this.picturePointsLayer) { 260 | this.picturePointsLayer.definitionExpression = "level_id = " + selectedFloor; 261 | } 262 | }); 263 | }); 264 | } 265 | 266 | onEnter() { 267 | this.selectedFloor = 1; 268 | 269 | if (this.floors) { 270 | this.floors.getItemAt(this.selectedFloor).activate(); 271 | } 272 | 273 | this.appState.view.environment.lighting.directShadowsEnabled = false; 274 | this.appState.view.environment.lighting.ambientOcclusionEnabled = false; 275 | this.oldDate = this.appState.view.environment.lighting.date; 276 | this.appState.view.environment.lighting.date = new Date("Thu Aug 01 2019 03:00:00 GMT+0200 (Central European Summer Time)"); 277 | this.handles.add(this.appState.view.on("click", (event: any) => { 278 | // the hitTest() checks to see if any graphics in the view 279 | // intersect the given screen x, y coordinates 280 | this.appState.view.hitTest(event) 281 | .then((response) => { 282 | const filtered = response.results.filter((result: any) => { 283 | return result.graphic.layer === this.picturePointsLayer; 284 | })[0]; 285 | if (filtered) { 286 | this.appState.popupInfo = new PopupInfo({ 287 | image: filtered.graphic.attributes.url, 288 | credit: filtered.graphic.attributes.title 289 | }); 290 | } 291 | }); 292 | }), "click"); 293 | 294 | this.legendWrapper.hide = !this.layer; 295 | 296 | if (this.layer) { 297 | this.layer.visible = true; 298 | } 299 | 300 | if (this.picturePointsLayer) { 301 | this.picturePointsLayer.visible = true; 302 | } 303 | 304 | } 305 | 306 | onLeave() { 307 | this.handles.remove("click"); 308 | this.appState.view.environment.lighting.directShadowsEnabled = true; 309 | this.appState.view.environment.lighting.ambientOcclusionEnabled = true; 310 | this.appState.view.environment.lighting.date = this.oldDate; 311 | 312 | this.legendWrapper.hide = true; 313 | 314 | if (this.layer) { 315 | this.layer.visible = false; 316 | } 317 | 318 | if (this.picturePointsLayer) { 319 | this.picturePointsLayer.visible = false; 320 | } 321 | } 322 | 323 | private getExtraInfoLayers() { 324 | if (this.appState && this.appState.view.map.layers.length > 0) { 325 | // Get the info points on the floors: 326 | if (!this.layer) { 327 | this.layer = appUtils.findLayer(this.appState.view.map.layers, this.layerNameForInfoPoint) as FeatureLayer; 328 | if (this.layer) { 329 | this.layer.visible = false; 330 | this.legendWrapper.legend.layerInfos = [ 331 | { 332 | layer: this.layer, 333 | title: "Legend", 334 | hideLayers: [] 335 | } 336 | ]; 337 | } 338 | } 339 | // Get extra pictures: 340 | if (!this.picturePointsLayer) { 341 | this.picturePointsLayer = appUtils.findLayer(this.appState.view.map.layers, this.layerNameForPicturePoint) as FeatureLayer; 342 | if (this.picturePointsLayer) { 343 | this.picturePointsLayer.visible = false; 344 | this.picturePointsLayer.outFields = ["*"]; 345 | } 346 | } 347 | 348 | } 349 | } 350 | } 351 | -------------------------------------------------------------------------------- /src/js/sections/HomeSection.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Esri 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | import { subclass, property } from "esri/core/accessorSupport/decorators"; 18 | import { tsx } from "esri/widgets/support/widget"; 19 | import Section from "./Section"; 20 | import AppState from "../AppState"; 21 | import { Timetable } from "../widgets/Timetable/Timetable"; 22 | import Viewpoints from "../widgets/Viewpoints/Viewpoints"; 23 | import * as watchUtils from "esri/core/watchUtils"; 24 | import Handles from "esri/core/Handles"; 25 | import FeatureLayer from "esri/layers/FeatureLayer"; 26 | import * as appUtils from "../support/appUtils"; 27 | import Collection from "esri/core/Collection"; 28 | import PopupInfo from "../widgets/Popup/PopupInfo"; 29 | import WebScene from "esri/WebScene"; 30 | 31 | interface HomeSectionCtorArgs { 32 | content?: (that: HomeSection) => any; 33 | timetable?: Timetable; 34 | title?: string; 35 | showExternalPoints?: boolean; 36 | } 37 | 38 | @subclass("sections/HomeSection") 39 | class HomeSection extends Section { 40 | @property() 41 | title = "Overview"; 42 | 43 | @property() 44 | id = "home"; 45 | 46 | @property({ constructOnly: true }) 47 | timetable: Timetable; 48 | 49 | @property() 50 | private textTitle: string; 51 | 52 | @property() 53 | appState: AppState; 54 | 55 | @property() 56 | infoPointsLayer: FeatureLayer; 57 | 58 | @property({ constructOnly: true }) 59 | showExternalPoints: boolean = false; 60 | 61 | private handles = new Handles(); 62 | 63 | @property() 64 | content: (that: this) => any = (that: this) => (this.appState.view.map as WebScene).portalItem.snippet; 65 | 66 | @property({dependsOn: ["appState"], readOnly: true}) 67 | get viewpoints() { 68 | return new Viewpoints({appState: this.appState}); 69 | } 70 | 71 | render() { 72 | const timetable = this.timetable ? (
73 |

Opening hours

74 |
75 | {this.timetable.render()} 76 |
77 |
) : null; 78 | const title = this.textTitle ? (

{this.textTitle}

) : null; 79 | 80 | return (
81 |
82 | {title} 83 | {this.content(this)} 84 |
85 | {timetable} 86 |
); 87 | } 88 | 89 | paneRight() { 90 | const viewpoints = this.viewpoints ? this.viewpoints.render() : null; 91 | return (
{viewpoints}
); 92 | } 93 | 94 | constructor(args: HomeSectionCtorArgs) { 95 | super(args as any); 96 | } 97 | 98 | postInitialize() { 99 | // Optionally add the external info points to display pictures: 100 | watchUtils.whenOnce(this, "appState", () => { 101 | watchUtils.on(this, "appState.view.map.layers", "change", () => { 102 | if (this.appState && this.appState.view.map.layers.length > 0) { 103 | this.infoPointsLayer = this.appState.view.map.layers.find(layer => layer.title.indexOf(appUtils.EXTERNAL_INFOPOINT_LAYER_PREFIX) > -1) as FeatureLayer; 104 | 105 | if (this.infoPointsLayer) { 106 | this.infoPointsLayer.visible = false; 107 | this.infoPointsLayer.outFields = ["*"]; 108 | this.infoPointsLayer.visible = false; 109 | this.infoPointsLayer.popupTemplate.overwriteActions = true; 110 | this.infoPointsLayer.popupTemplate.actions = new Collection(); 111 | } 112 | } 113 | }); 114 | }); 115 | 116 | // Get the title to display in the text: 117 | watchUtils.whenOnce(this, "appState.view.map.portalItem.title", () => { 118 | this.textTitle = (this.appState.view.map as WebScene).portalItem.title; 119 | }); 120 | 121 | // Enabling external point if we are in the home section: 122 | watchUtils.init(this, "appState.pageLocation", (l) => { 123 | if (this.infoPointsLayer) { 124 | this.infoPointsLayer.visible = this.showExternalPoints && l === "home"; 125 | } 126 | }); 127 | } 128 | 129 | onEnter() { 130 | // reset the active viewpoint each time we go in home section: 131 | this.viewpoints.activeViewpoint = null; 132 | 133 | // check if we click on an external point and display a popup if that is the case: 134 | this.handles.add(this.appState.view.on("click", (event: any) => { 135 | this.appState.view.hitTest(event) 136 | .then((response) => { 137 | const filtered = response.results.filter((result: any) => { 138 | return result.graphic.layer === this.infoPointsLayer; 139 | })[0]; 140 | if (filtered) { 141 | this.appState.popupInfo = new PopupInfo({ 142 | image: filtered.graphic.attributes.url, 143 | credit: filtered.graphic.attributes.title 144 | }) 145 | } 146 | }); 147 | }), "click"); 148 | } 149 | 150 | onLeave() { 151 | // when not in home, remove the click listener: 152 | this.handles.remove("click"); 153 | } 154 | } 155 | 156 | export = HomeSection; 157 | -------------------------------------------------------------------------------- /src/js/sections/Section.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Esri 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | import { subclass,property } from "esri/core/accessorSupport/decorators"; 18 | 19 | import Camera from "esri/Camera"; 20 | import Widget from "esri/widgets/Widget"; 21 | 22 | import AppState from "../AppState"; 23 | 24 | @subclass("sections/Section") 25 | abstract class Section extends Widget { 26 | @property() 27 | appState: AppState; 28 | 29 | @property() 30 | abstract title: string; 31 | 32 | @property() 33 | abstract id: string; 34 | 35 | @property() 36 | camera: Camera; 37 | 38 | @property() 39 | active: boolean = false; 40 | 41 | abstract render(): any; 42 | 43 | abstract paneRight(): any; 44 | 45 | onEnter() {} 46 | 47 | onLeave() {} 48 | 49 | postInitialize() { 50 | this.own(this.watch("camera", camera => { 51 | if (camera) { 52 | this.emit("go-to", camera); 53 | } 54 | })); 55 | } 56 | } 57 | 58 | export = Section; 59 | -------------------------------------------------------------------------------- /src/js/sections/Sections.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Esri 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | import { subclass, property } from "esri/core/accessorSupport/decorators"; 18 | import { tsx } from "esri/widgets/support/widget"; 19 | 20 | import Collection from "esri/core/Collection"; 21 | import Section from "./Section"; 22 | import AppState from "../AppState"; 23 | 24 | type SectionSubclass = Pick; 25 | 26 | @subclass("sections/Section") 27 | class Sections extends Collection { 28 | //-------------------------------------------------------------------------- 29 | // 30 | // Properties 31 | // 32 | //-------------------------------------------------------------------------- 33 | 34 | @property() 35 | set activeSection(sectionToActivate: SectionSubclass) { 36 | if (sectionToActivate !== this._get("activeSection")) { 37 | this.previousActiveSection = this.activeSection; 38 | 39 | if (this.previousActiveSection) { 40 | this.previousActiveSection.onLeave(); 41 | } 42 | 43 | this.forEach(section => { 44 | if (section !== sectionToActivate) { 45 | section.active = false; 46 | } 47 | else { 48 | section.active = true; 49 | } 50 | }); 51 | this.appState.pageLocation = sectionToActivate ? sectionToActivate.id : null; 52 | this._set("activeSection", sectionToActivate); 53 | 54 | if (this.activeSection) { 55 | this.activeSection.onEnter(); 56 | } 57 | } 58 | 59 | if (this.activeSection.camera) { 60 | this.emit("go-to", this.activeSection.camera); 61 | } 62 | } 63 | 64 | @property({ constructOnly: true}) 65 | private appState: AppState; 66 | 67 | //-------------------------------------------------------------------------- 68 | // 69 | // Variables 70 | // 71 | //-------------------------------------------------------------------------- 72 | 73 | previousActiveSection: SectionSubclass = null; 74 | 75 | activeSectionNode: HTMLElement = null; 76 | previousActiveSectionNode: HTMLElement = null; 77 | 78 | //-------------------------------------------------------------------------- 79 | // 80 | // Life circle 81 | // 82 | //-------------------------------------------------------------------------- 83 | 84 | constructor(sections: SectionSubclass[], appState: AppState) { 85 | super(sections.map((section) => { 86 | section.appState = appState; 87 | return section; 88 | })); 89 | 90 | this.appState = appState; 91 | 92 | this.watch("appState.pageLocation", this.activateSection); 93 | } 94 | 95 | //-------------------------------------------------------------------------- 96 | // 97 | // Public methods 98 | // 99 | //-------------------------------------------------------------------------- 100 | 101 | activateSection(section: string | number | SectionSubclass) { 102 | if (section instanceof Section) { 103 | this.activeSection = section; 104 | } 105 | if (typeof section === "string") { 106 | this.activeSection = this.find((s) => s.id === section); 107 | } 108 | if (typeof section === "number") { 109 | this.activeSection = this.getItemAt(section); 110 | } 111 | } 112 | 113 | public paneLeft(firstRendering = true) { 114 | const panes = this.swapPanes("render", firstRendering); 115 | return (
{panes}
); 116 | } 117 | 118 | public paneRight(firstRendering = true) { 119 | const panes = this.swapPanes("paneRight", firstRendering); 120 | return (
{panes}
); 121 | } 122 | 123 | public menu() { 124 | let items = this.map((section, i) => { 125 | const slash = i !== 0 ? (/ ) : null; 126 | return [slash, this.renderOneSectionMenu(section, i)]; 127 | }); 128 | return (); 129 | } 130 | 131 | private renderOneSectionMenu(section: SectionSubclass, i: number) { 132 | const classes = section.active? "active" : ""; 133 | return ( {this.activateSection(section.id);}}>{section.title}); 134 | } 135 | 136 | private swapPanes(renderViewToCall: string, firstRendering = true) { 137 | 138 | 139 | const activeSectionClasses = firstRendering ? "pane" : "active pane"; 140 | const previousActiveSectionClasses = firstRendering ? "active pane" : "pane"; 141 | 142 | const currentPane = this.activeSection ? (
{this.activeSection[renderViewToCall]()}
) : null; 143 | const previousUsedPane = this.previousActiveSection ? (
{this.previousActiveSection[renderViewToCall]()}
) : null; 144 | 145 | return (
{previousUsedPane}{currentPane}
); 146 | } 147 | } 148 | 149 | export = Sections; 150 | -------------------------------------------------------------------------------- /src/js/sections/SurroundingsSection.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Esri 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | import { subclass, property } from "esri/core/accessorSupport/decorators"; 18 | import { tsx } from "esri/widgets/support/widget"; 19 | 20 | import Section from "./Section"; 21 | import AppState from "../AppState"; 22 | import Collection from "esri/core/Collection"; 23 | import WebScene from "esri/WebScene"; 24 | import Camera from "esri/Camera"; 25 | import Widget from "esri/widgets/Widget"; 26 | import Toggle from "../widgets/Toggle/Toggle"; 27 | import * as watchUtils from "esri/core/watchUtils"; 28 | import FeatureLayer from "esri/layers/FeatureLayer"; 29 | import GroupLayer from "esri/layers/GroupLayer"; 30 | import * as appUtils from "../support/appUtils"; 31 | 32 | @subclass("SurroundingsElement") 33 | class SurroundingsElement extends Widget { 34 | @property() 35 | toggle = new Toggle(); 36 | 37 | @property({aliasOf: "toggle.active"}) 38 | set active(isActive: boolean) { 39 | this.toggle.active = isActive; 40 | } 41 | get active() { 42 | return this.toggle.active; 43 | } 44 | 45 | @property() 46 | title: string; 47 | 48 | @property() 49 | layer: FeatureLayer | GroupLayer; 50 | 51 | @property() 52 | appState: AppState; 53 | 54 | @property() 55 | camera: Camera; 56 | 57 | activate() { 58 | this.appState.view.goTo(this.camera); 59 | if (this.layer) { 60 | this.layer.visible = true; 61 | } 62 | } 63 | 64 | deactivate() { 65 | if (this.layer) { 66 | this.layer.visible = false; 67 | } 68 | } 69 | 70 | content() { 71 | return (
); 72 | } 73 | 74 | render() { 75 | return (
76 |

this.active = !this.active}> 77 | {this.toggle.render()} 78 | {this.title} 79 |

80 |
{this.content()}
81 |
); 82 | } 83 | 84 | constructor(args: any) { 85 | super(args); 86 | 87 | if (args.content) { 88 | this.content = args.content.bind(this); 89 | } 90 | 91 | this.watch("active", (isActive) => { 92 | if (isActive) { 93 | this.activate(); 94 | } 95 | else { 96 | this.deactivate(); 97 | } 98 | }); 99 | } 100 | } 101 | 102 | @subclass("PoIElement") 103 | class PoIElement extends Widget { 104 | @property() 105 | camera: Camera; 106 | 107 | @property() 108 | name: string; 109 | 110 | constructor(args: {name: string, camera: Camera, appState: AppState}) { 111 | super(args as any); 112 | } 113 | 114 | @property() 115 | appState: AppState; 116 | 117 | render() { 118 | return ( 119 | 120 | ); 121 | } 122 | 123 | onClick(event: Event) { 124 | this.appState.view.goTo(this.camera); 125 | } 126 | } 127 | 128 | @subclass("sections/SurroundingsSection") 129 | class SurroundingsSection extends Section { 130 | @property() 131 | title = "Surroundings"; 132 | 133 | @property() 134 | appState: AppState; 135 | 136 | @property() 137 | id = "surroundings"; 138 | 139 | @property() 140 | poiElements: Collection; 141 | 142 | @property({dependsOn: ["appState.view.map.layers", "poiElements"], readOnly: true}) 143 | get elements() { 144 | if (this.appState && this.appState.view.map.layers.length > 0) { 145 | const elements = this.appState.view.map.layers 146 | .filter(layer => layer.title.indexOf(appUtils.SURROUNDINGS_LAYER_PREFIX) > -1) 147 | .map(layer => { 148 | 149 | layer.visible = false; 150 | 151 | return new SurroundingsElement({ 152 | title: layer.title.replace("Surroundings: ", ""), 153 | layer: layer, 154 | appState: this.appState, 155 | camera: this.camera 156 | }); 157 | }); 158 | 159 | if (this.poiElements.length > 0) { 160 | elements.push(new SurroundingsElement({ 161 | title: "Points of Interest", 162 | appState: this.appState, 163 | camera: this.camera, 164 | content: () => { 165 | const poiElementsItems = this.poiElements.map(el => el.render()); 166 | return (
167 | {poiElementsItems.toArray()} 168 |
); 169 | } 170 | })); 171 | } 172 | 173 | return elements; 174 | } 175 | else { 176 | return new Collection(); 177 | } 178 | 179 | 180 | } 181 | 182 | constructor(args: any) { 183 | super(args); 184 | 185 | this.own(watchUtils.whenOnce(this, "appState", () => { 186 | 187 | (this.appState.view.map as WebScene).when(() => { 188 | 189 | // Get the point of interests: 190 | this.poiElements = (this.appState.view.map as WebScene).presentation.slides 191 | .filter(slide => slide.title.text.indexOf("Points of Interest:") > -1) 192 | .map(slide => { 193 | (this.appState.view.map as WebScene).presentation.slides.remove(slide); 194 | return new PoIElement({ 195 | name: slide.title.text.replace("Points of Interest: ", ""), 196 | camera: slide.viewpoint.camera, 197 | appState: this.appState 198 | }); 199 | }); 200 | 201 | watchUtils.on(this.appState, "view.map.layers", "change", () => this.notifyChange("elements")); 202 | watchUtils.on(this, "poiElements", "change", () => this.notifyChange("elements")); 203 | }); 204 | })); 205 | } 206 | 207 | render() { 208 | return (
209 |

Surroundings

210 | {this.elements.map(l => l.render()).toArray()} 211 |
); 212 | } 213 | 214 | paneRight() { 215 | return (
); 216 | } 217 | 218 | onEnter() { 219 | this.elements.forEach(el => el.active = el.title === "Points of Interest"); 220 | } 221 | 222 | onLeave() { 223 | this.elements.forEach(e => e.active = false); 224 | } 225 | } 226 | 227 | export = SurroundingsSection; 228 | -------------------------------------------------------------------------------- /src/js/sections/css/sections.scss: -------------------------------------------------------------------------------- 1 | @import "../../../css/variables.scss"; 2 | @import "../../../css/mixins.scss"; 3 | 4 | /*************** 5 | Slide animation 6 | ****************/ 7 | 8 | .side-container .pane { 9 | @include transition(all 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55), opacity 0.6s cubic-bezier(0.42,0,0.58,1)); 10 | position: absolute; 11 | opacity: 0; 12 | top: 0; 13 | } 14 | 15 | .side-container.left .pane { 16 | left: -1000px; 17 | width: 390px; 18 | } 19 | 20 | .side-container.right .pane { 21 | right: -1000px; 22 | width: 190px; 23 | } 24 | 25 | .side-container.right .pane.active { 26 | right: 0; 27 | opacity: 1; 28 | } 29 | 30 | .side-container.left .pane.active { 31 | left: 0; 32 | opacity: 1; 33 | } 34 | 35 | .side-container.left .pane h1 { 36 | @include transition(margin 0.7s cubic-bezier(0.68, -0.55, 0.265, 1.55)); 37 | margin-left: -200px; 38 | } 39 | 40 | .side-container.left .pane.active h1 { 41 | margin-left: 0; 42 | } 43 | 44 | .side-container.left .pane h2 { 45 | @include transition(margin 0.8s ease); 46 | margin-left: -200px; 47 | } 48 | 49 | .side-container.left .pane h2:nth-child(2) { 50 | @include transition(margin 0.8s ease 0.3s); 51 | } 52 | 53 | .side-container.left .pane h2:nth-child(3) { 54 | @include transition(margin 0.8s ease 0.6s); 55 | } 56 | 57 | .side-container.left .pane.active h2 { 58 | margin-left: 0; 59 | } 60 | 61 | /*************** 62 | Surroundings 63 | ****************/ 64 | 65 | #surroundings { 66 | .element { 67 | width: 100%; 68 | @include transition(margin 0.3s ease); 69 | margin-left: -200px; 70 | 71 | a { 72 | color: $primaryColor; 73 | vertical-align: -2px; 74 | margin-left: 20px; 75 | text-decoration: none; 76 | } 77 | 78 | &.active { 79 | a { 80 | color: $orange; 81 | } 82 | } 83 | } 84 | 85 | .slash-title.width-toggle { 86 | width: 100%; 87 | 88 | &::before { 89 | display: none; 90 | } 91 | } 92 | 93 | // .toggle { 94 | // border-color: $primaryColor; 95 | 96 | // .knob { 97 | // background-color: $primaryColor; 98 | // } 99 | // } 100 | } 101 | 102 | .active #surroundings { 103 | .element { 104 | margin-left: 0; 105 | } 106 | 107 | .element:nth-child(1) { 108 | @include transition-delay(0.2s); 109 | } 110 | 111 | .element:nth-child(2) { 112 | @include transition-delay(0.3s); 113 | } 114 | 115 | .element:nth-child(3) { 116 | @include transition-delay(0.4s); 117 | } 118 | } 119 | 120 | /*************** 121 | Floors 122 | ****************/ 123 | 124 | #floors { 125 | pointer-events: all; 126 | h1 { 127 | margin-left: 120px; 128 | // width: 129 | pointer-events: all; 130 | width: 600px; 131 | } 132 | 133 | h1.number { 134 | margin-left: 0; 135 | margin-top: 25px; 136 | color: $orange; 137 | } 138 | 139 | .level, .number { 140 | position: absolute; 141 | } 142 | 143 | .level { 144 | margin-top: 61px; 145 | z-index: 2; 146 | margin-left: 28px; 147 | opacity: 0.8; 148 | } 149 | 150 | .number { 151 | font-size: 200px; 152 | z-index: 1; 153 | } 154 | 155 | h3.subtitle { 156 | font-family: "Avenir Next"; 157 | font-weight: normal; 158 | font-style: italic; 159 | font-size: 40px; 160 | letter-spacing: 8px; 161 | opacity: 0.3; 162 | margin-top: -30px; 163 | margin-left: 125px; 164 | pointer-events: all; 165 | } 166 | 167 | .italic { 168 | font-style: italic; 169 | opacity: 0.5; 170 | } 171 | 172 | .content { 173 | margin-top: 50px; 174 | } 175 | 176 | .play_button { 177 | $play_button_background: scale-color($primaryColor, $lightness: -70%); 178 | position: relative; 179 | background: $play_button_background; 180 | border: none; 181 | outline: none; 182 | padding: 6px; 183 | border-radius: 13px; 184 | cursor: pointer; 185 | margin-left: 5px; 186 | vertical-align: -2px; 187 | border: 1px solid $primaryColor; 188 | 189 | .play_button__icon { 190 | $size: 12px; 191 | height: $size; 192 | width: $size; 193 | line-height: $size; 194 | position: relative; 195 | left: 1px; 196 | z-index: 0; 197 | box-sizing: border-box; 198 | display: inline-block; 199 | overflow: hidden; 200 | 201 | &:before, &:after { 202 | content: ''; 203 | position: absolute; 204 | transition: 0.3s; 205 | background: #FFF; 206 | height: 100%; 207 | width: 50%; 208 | top: 0; 209 | } 210 | 211 | &:before { 212 | left: 0; 213 | } 214 | 215 | &:after { 216 | right: 0; 217 | } 218 | } 219 | 220 | .play_button__mask { 221 | position: absolute; 222 | z-index: 1; 223 | left: 0; 224 | top: 0; 225 | width: 100%; 226 | height: 100%; 227 | display: block; 228 | 229 | &:before, &:after { 230 | content: ''; 231 | position: absolute; 232 | left: 0; 233 | height: 100%; 234 | width: 150%; 235 | background: $play_button_background; 236 | transition: all 0.3s ease-out; 237 | } 238 | 239 | &:before { 240 | top: -100%; 241 | transform-origin: 0% 100%; 242 | transform: rotate(26.5deg); 243 | } 244 | 245 | &:after { 246 | transform-origin: 0% 0%; 247 | transform: rotate(-26.5deg); 248 | top: 100%; 249 | } 250 | } 251 | 252 | &.playing { 253 | .play_button__icon { 254 | left: 0; 255 | 256 | &:before { 257 | transform: translateX(-25%); 258 | } 259 | 260 | &:after { 261 | transform: translateX(25%); 262 | } 263 | } 264 | 265 | .play_button__mask { 266 | &:before, &:after { 267 | transform: rotate(0); 268 | } 269 | } 270 | } 271 | } 272 | } 273 | 274 | #floorLegend { 275 | position: absolute; 276 | bottom: 30px; 277 | left: 15px; 278 | 279 | &.hide { 280 | display: none; 281 | } 282 | 283 | div { 284 | background: transparent; 285 | color: #fff; 286 | font-size: 15px; 287 | line-height: 22px; 288 | } 289 | 290 | .esri-legend__layer-caption, .esri-widget__heading { 291 | display: none; 292 | } 293 | 294 | .esri-legend__layer-row { 295 | height: 28px; 296 | display: block; 297 | } 298 | 299 | svg { 300 | transform: scale(0.7); 301 | } 302 | } 303 | 304 | #surroundings { 305 | .active .content { 306 | display: block; 307 | 308 | a { 309 | color: #fff; 310 | } 311 | 312 | & > div { 313 | a { 314 | @include transition(all 0.8s); 315 | } 316 | svg { 317 | @include transition(all 0.8s); 318 | } 319 | } 320 | 321 | & > div:hover { 322 | a { 323 | color: #fff; 324 | } 325 | 326 | svg { 327 | opacity: 1; 328 | } 329 | } 330 | } 331 | 332 | .content { 333 | pointer-events: all; 334 | margin-top: -30px; 335 | font-size: 25px; 336 | line-height: 40px; 337 | margin-left: 25px; 338 | display: none; 339 | 340 | 341 | 342 | svg { 343 | width: 15px; 344 | opacity: 0.5; 345 | } 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /src/js/support/BuildingVisualisation.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Esri 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | import { subclass, property } from "esri/core/accessorSupport/decorators"; 18 | 19 | // Esri 20 | import Accessor from "esri/core/Accessor"; 21 | import BuildingSceneLayer from "esri/layers/BuildingSceneLayer"; 22 | import * as watchUtils from "esri/core/watchUtils"; 23 | import Renderer from "esri/renderers/Renderer"; 24 | import { createFilterFor, FLOOR_FILTER_NAME, definitionExpressions } from "./visualVariables"; 25 | 26 | // App 27 | import AppState from "../AppState"; 28 | import * as buildingSceneLayerUtils from "./buildingSceneLayerUtils"; 29 | 30 | interface BuildingVisualisationCtorArgs { 31 | layer: BuildingSceneLayer; 32 | appState: AppState; 33 | customBaseRenderer?: any; 34 | floorMapping?: (originalFloor: number) => number; 35 | extraQuery?: string; 36 | } 37 | 38 | @subclass("support/BuildingVisualisation") 39 | class BuildingVisualisation extends Accessor { 40 | //-------------------------------------------------------------------------- 41 | // 42 | // Properties 43 | // 44 | //-------------------------------------------------------------------------- 45 | @property() 46 | layer: BuildingSceneLayer; 47 | 48 | private initialRenderer: HashMap = {}; 49 | 50 | @property({ 51 | readOnly: true, 52 | dependsOn: [ 53 | "appState.pageLocation", 54 | "appState.floorNumber" 55 | ] 56 | }) 57 | get layerRenderer() { 58 | return buildingSceneLayerUtils 59 | .getVisualVarsFromAppState( 60 | this.appState, 61 | "mainBuilding", 62 | "renderer" 63 | ); 64 | } 65 | 66 | @property() 67 | customBaseRenderer: Renderer; 68 | 69 | @property({ 70 | readOnly: true, 71 | dependsOn: [ 72 | "appState.pageLocation", 73 | "appState.floorNumber" 74 | ] 75 | }) 76 | get buildingFilters() { 77 | if (this.appState.pageLocation === "floors") { 78 | return createFilterFor(this.floorMapping(this.appState.floorNumber), this.extraQuery); 79 | } 80 | return null; 81 | } 82 | 83 | @property({ constructOnly: true }) 84 | appState: AppState; 85 | 86 | @property() 87 | extraQuery: string; 88 | 89 | //-------------------------------------------------------------------------- 90 | // 91 | // Life circle 92 | // 93 | //-------------------------------------------------------------------------- 94 | constructor(args: BuildingVisualisationCtorArgs) { 95 | super(); 96 | 97 | this.appState = args.appState; 98 | 99 | this.layer = args.layer; 100 | 101 | if (args.floorMapping) { 102 | this.floorMapping = args.floorMapping; 103 | } 104 | 105 | if (args.extraQuery) { 106 | this.extraQuery = args.extraQuery; 107 | } 108 | 109 | // Save the initial renderers, so that we can set it back: 110 | buildingSceneLayerUtils.goThroughSubLayers(args.layer, (sublayer) => { 111 | if (sublayer.type === "building-component") { 112 | this.initialRenderer[sublayer.title] = (sublayer as any).renderer; 113 | } 114 | }); 115 | 116 | // To improve performance, we will set a definition expression that will 117 | // force the api to load the data for floor attribute: 118 | buildingSceneLayerUtils.goThroughSubLayers(args.layer, (sublayer) => { 119 | if (sublayer.type === "building-component") { 120 | sublayer.definitionExpression = definitionExpressions.basic; 121 | } 122 | }); 123 | 124 | watchUtils.init(this, "layerRenderer", this._updateBaseRenderer); 125 | watchUtils.init(this, "customBaseRenderer", this._updateBaseRenderer); 126 | 127 | // Set the building filters when necessary: 128 | watchUtils.init(this, "buildingFilters", (buildingFilters) => { 129 | if (!this.appState.pageLocation || this.appState.pageLocation !== "floors") { 130 | this.layer.activeFilterId = null; 131 | } 132 | else { 133 | const currentFilter = this.layer.filters.find((filter: any) => filter.name === FLOOR_FILTER_NAME); 134 | if (currentFilter) { 135 | this.layer.filters.remove(currentFilter); 136 | } 137 | this.layer.filters.push(buildingFilters); 138 | this.layer.activeFilterId = this.layer.filters.find((filter: any) => filter.name === FLOOR_FILTER_NAME).id; 139 | } 140 | }); 141 | 142 | } 143 | 144 | normalizeCtorArgs(args: BuildingVisualisationCtorArgs) { 145 | return { 146 | appState: args.appState 147 | }; 148 | } 149 | 150 | 151 | //-------------------------------------------------------------------------- 152 | // 153 | // Private Methods 154 | // 155 | //-------------------------------------------------------------------------- 156 | 157 | private _updateBaseRenderer() { 158 | if (this.customBaseRenderer) { 159 | buildingSceneLayerUtils.updateSubLayers(this.layer, ["renderer"], this.customBaseRenderer); 160 | } 161 | else if (!this.appState.pageLocation || this.appState.pageLocation === "home" || this.appState.pageLocation === "custom") { 162 | buildingSceneLayerUtils.goThroughSubLayers(this.layer, (sublayer) => { 163 | if (sublayer.type === "building-component") { 164 | sublayer.renderer = this.initialRenderer[sublayer.title] && (this.initialRenderer[sublayer.title] as any).clone(); 165 | } 166 | }); 167 | } 168 | else { 169 | buildingSceneLayerUtils.updateSubLayers(this.layer, ["renderer"], this.layerRenderer); 170 | } 171 | } 172 | 173 | private floorMapping(originalFloor: number) { 174 | return originalFloor; 175 | } 176 | 177 | } 178 | 179 | export = BuildingVisualisation; 180 | -------------------------------------------------------------------------------- /src/js/support/SurroundingsVisualisation.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Esri 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | import { subclass, property } from "esri/core/accessorSupport/decorators"; 18 | import Accessor from "esri/core/Accessor"; 19 | 20 | // App 21 | import AppState from "../AppState"; 22 | import * as buildingSceneLayerUtils from "./buildingSceneLayerUtils"; 23 | import * as watchUtils from "esri/core/watchUtils"; 24 | import Renderer from "esri/renderers/Renderer"; 25 | import SceneLayer from "esri/layers/SceneLayer"; 26 | 27 | @subclass("support/SurroundingsVisualisation") 28 | class SurroundingsVisualisation extends Accessor { 29 | //-------------------------------------------------------------------------- 30 | // 31 | // Properties 32 | // 33 | //-------------------------------------------------------------------------- 34 | 35 | @property({ 36 | readOnly: true, 37 | dependsOn: [ 38 | "appState.pageLocation", 39 | "appState.floorNumber" 40 | ] 41 | }) 42 | get surroundingsRenderer() { 43 | return buildingSceneLayerUtils 44 | .getVisualVarsFromAppState( 45 | this.appState, 46 | "surroundings", 47 | "renderer" 48 | ); 49 | } 50 | 51 | @property() 52 | customRenderer: Renderer; 53 | 54 | @property({ 55 | readOnly: true, 56 | dependsOn: [ 57 | "appState.pageLocation", 58 | "appState.floorNumber" 59 | ] 60 | }) 61 | get surroundingsOpacity() { 62 | return buildingSceneLayerUtils 63 | .getVisualVarsFromAppState( 64 | this.appState, 65 | "surroundings", 66 | "opacity" 67 | ); 68 | } 69 | 70 | @property() 71 | layer: SceneLayer; 72 | 73 | @property({ constructOnly: true}) 74 | appState: AppState; 75 | 76 | //-------------------------------------------------------------------------- 77 | // 78 | // Life circle 79 | // 80 | //-------------------------------------------------------------------------- 81 | 82 | constructor(args: {layer: SceneLayer, appState: AppState}) { 83 | super(); 84 | 85 | this.appState = args.appState; 86 | this.layer = args.layer; 87 | 88 | this.layer.when(() => { 89 | watchUtils.init(this, "surroundingsRenderer", this._updateBaseRenderer); 90 | watchUtils.init(this, "customRenderer", this._updateBaseRenderer); 91 | 92 | watchUtils.init(this, "surroundingsOpacity", (surroundingsOpacity) => { 93 | this.layer.opacity = surroundingsOpacity; 94 | }); 95 | }); 96 | } 97 | 98 | //-------------------------------------------------------------------------- 99 | // 100 | // Private Methods 101 | // 102 | //-------------------------------------------------------------------------- 103 | 104 | private _updateBaseRenderer() { 105 | if (this.customRenderer) { 106 | this.layer.renderer = this.customRenderer; 107 | } 108 | else { 109 | this.layer.renderer = this.surroundingsRenderer; 110 | } 111 | } 112 | } 113 | 114 | export = SurroundingsVisualisation; 115 | -------------------------------------------------------------------------------- /src/js/support/appUtils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Esri 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | import WebScene from "esri/WebScene"; 18 | import SceneView from "esri/views/SceneView"; 19 | import Layer from "esri/layers/Layer"; 20 | import Collection from "esri/core/Collection"; 21 | import PortalItem from "esri/portal/PortalItem"; 22 | import Portal from "esri/portal/Portal"; 23 | 24 | export function createViewFromWebScene(args: { 25 | mapContainer: string, 26 | websceneId: string, 27 | portalUrl?: string 28 | }) { 29 | 30 | const portalItem = new PortalItem({ 31 | id: args.websceneId 32 | }); 33 | 34 | // Let user add portal parameter 35 | if (args.portalUrl) { 36 | portalItem.portal = new Portal({ 37 | url: args.portalUrl 38 | }); 39 | } 40 | 41 | // Load webscene and display it in a SceneView 42 | const webscene = new WebScene({ 43 | portalItem 44 | }); 45 | 46 | const view = new SceneView({ 47 | container: args.mapContainer, 48 | map: webscene 49 | }); 50 | 51 | view.when(() => { 52 | view.padding = { left: 300 }; 53 | view.popup.autoOpenEnabled = false; 54 | }); 55 | 56 | // Remove default ui: 57 | view.ui.empty("top-left"); 58 | view.ui.empty("bottom-left"); 59 | 60 | return view; 61 | } 62 | 63 | 64 | export function findLayer(layers: Collection, title: string) { 65 | return layers.find(l => l.title === title); 66 | } 67 | 68 | export const CITY_LAYER_PREFIX = "City model"; 69 | export const MAIN_LAYER_PREFIX = "Building"; 70 | export const FLOOR_POINTS_LAYER_PREFIX = "Floor points"; 71 | export const INTERNAL_INFOPOINTS_LAYER_PREFIX = "Floor pictures"; 72 | export const EXTERNAL_INFOPOINT_LAYER_PREFIX = "External pictures"; 73 | export const SURROUNDINGS_LAYER_PREFIX = "Surroundings:"; 74 | -------------------------------------------------------------------------------- /src/js/support/buildingSceneLayerUtils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Esri 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | import BuildingSceneLayer from "esri/layers/BuildingSceneLayer"; 18 | import BuildingComponentSublayer from "esri/layers/buildingSublayers/BuildingComponentSublayer"; 19 | import BuildingGroupSublayer from "esri/layers/buildingSublayers/BuildingGroupSublayer"; 20 | import { renderers } from "./visualVariables"; 21 | import AppState from "../AppState"; 22 | 23 | export function updateSubLayersSymbolLayer (buildingLayer: BuildingSceneLayer, propertyPath: string[], value: any) { 24 | buildingLayer.when(function() { 25 | buildingLayer.allSublayers.forEach(function(layer) { 26 | if (layer instanceof BuildingComponentSublayer && (layer.renderer as any).clone) { 27 | const renderer = (layer.renderer as any).clone(); 28 | let parentProp = renderer.symbol.symbolLayers.getItemAt(0); 29 | propertyPath.forEach(function (prop, i) { 30 | if (i === (propertyPath.length - 1)) { 31 | parentProp[prop] = value; 32 | } 33 | else { 34 | parentProp = parentProp[prop]; 35 | } 36 | }); 37 | layer.renderer = renderer; 38 | } 39 | }); 40 | }); 41 | } 42 | 43 | export function updateSubLayers(buildingLayer: BuildingSceneLayer, propertyPath: string[], value: any) { 44 | buildingLayer.when(function() { 45 | buildingLayer.allSublayers.forEach(function(layer) { 46 | let parentProp = layer; 47 | propertyPath.forEach(function (prop, i) { 48 | if (i === (propertyPath.length - 1)) { 49 | parentProp[prop] = value; 50 | } 51 | else { 52 | parentProp = parentProp[prop]; 53 | } 54 | }); 55 | }); 56 | }); 57 | } 58 | 59 | export function goThroughSubLayers(buildingLayer: BuildingSceneLayer, callback: (sublayers: BuildingComponentSublayer | BuildingGroupSublayer) => void) { 60 | buildingLayer.when(function() { 61 | buildingLayer.allSublayers.forEach(function(layer) { 62 | callback(layer); 63 | }); 64 | }); 65 | } 66 | 67 | export function getVisualVarsFromAppState(appState: AppState, layerName: string, propertyName: string) { 68 | const defaultProps = renderers[layerName]["default"][propertyName]; 69 | const customPage = renderers[layerName][appState.pageLocation] ? renderers[layerName][appState.pageLocation][propertyName] : undefined; 70 | 71 | if (customPage !== undefined) { 72 | return customPage; 73 | } 74 | 75 | return defaultProps; 76 | } 77 | -------------------------------------------------------------------------------- /src/js/support/visualVariables.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Esri 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | import SimpleRenderer from "esri/renderers/SimpleRenderer"; 18 | 19 | export const renderers = { 20 | surroundings: { 21 | //-------------------------------------------------------------------------- 22 | // 23 | // Surroundings 24 | // 25 | //-------------------------------------------------------------------------- 26 | 27 | // This is used when displaying the different pages and 28 | // when there is no other variables defined 29 | default: { 30 | renderer: { 31 | type: "simple", 32 | symbol: { 33 | type: "mesh-3d", 34 | symbolLayers: [{ 35 | type: "fill", 36 | material: { color: [100,100,100, 1], colorMixMode: "replace" }, 37 | edges: { 38 | type: "solid", // autocasts as new SolidEdges3D() 39 | color: [30, 30, 30, 1] 40 | } 41 | }] 42 | } 43 | }, 44 | // Opacity when displaying the different pages and 45 | opacity: 1 46 | }, 47 | 48 | "surroundings": { 49 | renderer: { 50 | type: "simple", 51 | symbol: { 52 | type: "mesh-3d", 53 | // castShadows: false, 54 | symbolLayers: [{ 55 | type: "fill", 56 | material: { color: [255,255,255, 1], colorMixMode: "tint" }, 57 | edges: { 58 | type: "solid", // autocasts as new SolidEdges3D() 59 | color: [30, 30, 30, 1] 60 | } 61 | }] 62 | } 63 | } as any 64 | }, 65 | 66 | "floors": { 67 | opacity: 0 68 | } 69 | }, 70 | 71 | mainBuilding: { 72 | //-------------------------------------------------------------------------- 73 | // 74 | // Building 75 | // 76 | //-------------------------------------------------------------------------- 77 | 78 | // This is used when displaying the different pages and 79 | // when there is no other variables defined 80 | default: { 81 | renderer: new SimpleRenderer({ 82 | symbol: { 83 | type: "mesh-3d", 84 | symbolLayers: [{ 85 | type: "fill", 86 | material: { color: [255,184,1, 1], colorMixMode: "replace" }, 87 | edges: { 88 | type: "solid", // autocasts as new SolidEdges3D() 89 | color: [0, 0, 0, 1] 90 | } 91 | }] 92 | } 93 | } as any), 94 | // Opacity when displaying the different pages and 95 | opacity: 1 96 | }, 97 | 98 | // This is used when displaying the different floors: 99 | "floors": { 100 | renderer: new SimpleRenderer({ 101 | symbol: { 102 | type: "mesh-3d", 103 | symbolLayers: [{ 104 | type: "fill", 105 | material: { color: [255,255,255, 1], colorMixMode: "replace" }, 106 | edges: { 107 | type: "solid", // autocasts as new SolidEdges3D() 108 | color: [30, 30, 30, 1] 109 | } 110 | }] 111 | } 112 | } as any) 113 | }, 114 | 115 | "surroundings": { 116 | renderer: null as any 117 | } 118 | } 119 | }; 120 | 121 | // Some useful definitionExpression: 122 | export const definitionExpressions = { 123 | basic: "BldgLevel IS NULL OR BldgLevel IS NOT NULL", 124 | 125 | // this is used to filter FeatureLayer: 126 | floor: function (floorNumber: number, extraQuery = " AND Category <> 'Generic Models'") { 127 | return "BldgLevel = " + floorNumber + extraQuery; 128 | } 129 | }; 130 | 131 | export const FLOOR_FILTER_NAME = "BuildingFloor"; 132 | 133 | export function createFilterFor(floorNumber: number, extraQuery?: string) /*: BuildingFilter*/ { 134 | return { 135 | filterBlocks: [ 136 | { 137 | filterMode: { type: "solid" }, 138 | filterExpression: definitionExpressions.floor(floorNumber, extraQuery), 139 | title: "floor" 140 | } 141 | ], 142 | name: FLOOR_FILTER_NAME 143 | }; 144 | } 145 | -------------------------------------------------------------------------------- /src/js/widgets/FloorSelector/FloorSelector.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Esri 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | import { subclass, property } from "esri/core/accessorSupport/decorators"; 18 | import { tsx } from "esri/widgets/support/widget"; 19 | import Widget from "esri/widgets/Widget"; 20 | import AppState from "../../AppState"; 21 | 22 | 23 | interface FloorSelectorCtorArgs { 24 | minFloor: number; 25 | maxFloor: number; 26 | appState: AppState; 27 | } 28 | 29 | interface FloorSelectorCtorArgs2 { 30 | appState: AppState; 31 | } 32 | 33 | @subclass("widgets/FloorSelector") 34 | class FloorSelector extends Widget { 35 | @property({aliasOf: "appState.floorNumber"}) 36 | activeFloor: number; 37 | 38 | @property() 39 | maxFloor = 4; 40 | 41 | @property() 42 | minFloor = 0; 43 | 44 | @property({constructOnly: true}) 45 | appState: AppState; 46 | 47 | render() { 48 | const levels = Array.from(Array(Math.abs(this.minFloor) + this.maxFloor + 1).keys()).reverse().map((rawLevel: number) => { 49 | const level: number = rawLevel - this.minFloor; 50 | const levelText = level === 0 ? "G" : level; 51 | const activeClass = { 52 | "active": this.activeFloor === level 53 | }; 54 | return (
  • this.activeLevel(level)}>{levelText}
  • ); 55 | }); 56 | 57 | return (
    58 |

    Select floor

    59 |
      {levels}
    60 |
    ); 61 | } 62 | 63 | private activeLevel(newLevel: number) { 64 | event.stopPropagation(); 65 | this.activeFloor = newLevel; 66 | } 67 | 68 | constructor(args: FloorSelectorCtorArgs | FloorSelectorCtorArgs2) { 69 | super(args as any); 70 | } 71 | } 72 | 73 | export = FloorSelector; 74 | -------------------------------------------------------------------------------- /src/js/widgets/FloorSelector/css/floorSelector.scss: -------------------------------------------------------------------------------- 1 | @use "sass:math"; 2 | @import "../../../../css/variables.scss"; 3 | @import "../../../../css/mixins.scss"; 4 | 5 | $transition-time: 0.5s; 6 | 7 | .floor-selector { 8 | pointer-events: all; 9 | 10 | .level { 11 | font-family: 'Roboto Condensed', sans-serif; // import font! 12 | cursor: pointer; 13 | list-style: none; 14 | display: block; 15 | pointer-events: all; 16 | width: 100%; 17 | font-size: $floorSelectorLevelFontSize; 18 | text-align: right; 19 | @include transition(all $transition-time); 20 | margin-left: 200px; 21 | 22 | &:hover { 23 | font-size: $floorSelectorLevelOverFontSize; 24 | } 25 | 26 | &:first-child { 27 | margin-top: -$floorSelectorLevelFontSize - 15px; 28 | } 29 | 30 | &.active { 31 | font-size: $floorSelectorLevelActiveFontSize; 32 | margin-top: math.div(-$floorSelectorLevelActiveFontSize, 2) + 15px; 33 | margin-bottom: math.div(-$floorSelectorLevelActiveFontSize, 2) + 15px; 34 | color: #F6A803; 35 | text-transform: uppercase; 36 | pointer-events: all; 37 | 38 | &:first-child { 39 | margin-top: -$floorSelectorLevelActiveFontSize + 10px; 40 | } 41 | } 42 | } 43 | } 44 | 45 | .active .floor-selector { 46 | .level { 47 | margin-left: 0; 48 | 49 | &.active { 50 | margin-left: 15px; 51 | } 52 | } 53 | 54 | .level:nth-child(1) { 55 | @include transition(all $transition-time, margin-left 0.8s 0.1s); 56 | } 57 | 58 | .level:nth-child(2) { 59 | @include transition(all $transition-time, margin-left 0.8s 0.2s); 60 | } 61 | 62 | .level:nth-child(3) { 63 | @include transition(all $transition-time, margin-left 0.8s 0.3s); 64 | } 65 | 66 | .level:nth-child(4) { 67 | @include transition(all $transition-time, margin-left 0.8s 0.4s); 68 | } 69 | 70 | .level:nth-child(5) { 71 | @include transition(all $transition-time, margin-left 0.8s 0.5s); 72 | } 73 | 74 | .level:nth-child(6) { 75 | @include transition(all $transition-time, margin-left 0.8s 0.6s); 76 | } 77 | 78 | .level:nth-child(7) { 79 | @include transition(all $transition-time, margin-left 0.8s 0.7s); 80 | } 81 | 82 | .level:nth-child(8) { 83 | @include transition(all $transition-time, margin-left 0.8s 0.8s); 84 | } 85 | 86 | .level:nth-child(9) { 87 | @include transition(all $transition-time, margin-left 0.8s 0.9s); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/js/widgets/Popup/Popup.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Esri 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | import { subclass, property } from "esri/core/accessorSupport/decorators"; 18 | import { tsx } from "esri/widgets/support/widget"; 19 | import AppState from "../../AppState"; 20 | import Widget from "esri/widgets/Widget"; 21 | 22 | @subclass("widgets/Popup") 23 | class Popup extends Widget { 24 | @property({aliasOf: "appState.popupInfo.active"}) 25 | active: boolean = false; 26 | 27 | @property({aliasOf: "appState.popupInfo.image"}) 28 | image: string; 29 | 30 | @property({aliasOf: "appState.popupInfo.credit"}) 31 | credit: string; 32 | 33 | @property() 34 | appState: AppState; 35 | 36 | constructor(args: {appState: AppState, container: string}) { 37 | super(args); 38 | } 39 | 40 | render() { 41 | const activeClass = { 42 | "active": this.active 43 | }; 44 | const image = this.image ? () : null; 45 | const credit = this.credit ? (
    {this.credit}
    ) : null; 46 | return (
    {image}{credit}
    ); 47 | } 48 | 49 | private onClick(event: Event) { 50 | event.stopPropagation(); 51 | this.active = false; 52 | } 53 | } 54 | 55 | export = Popup; 56 | -------------------------------------------------------------------------------- /src/js/widgets/Popup/PopupInfo.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Esri 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | import { subclass, property } from "esri/core/accessorSupport/decorators"; 18 | import Accessor from "esri/core/Accessor"; 19 | 20 | @subclass("PopupInfo") 21 | class PopupInfo extends Accessor { 22 | @property() 23 | image: string; 24 | 25 | @property() 26 | credit: string; 27 | 28 | @property() 29 | active: boolean = true; 30 | } 31 | 32 | export = PopupInfo; 33 | -------------------------------------------------------------------------------- /src/js/widgets/Popup/css/popup.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../css/variables.scss"; 2 | @import "../../../../css/mixins.scss"; 3 | 4 | #appDiv #popup { 5 | z-index: 100; 6 | position: absolute; 7 | width: 0%; 8 | height: 0%; 9 | overflow: hidden; 10 | left: 50%; 11 | top: 50%; 12 | background: #fff; 13 | opacity: 0; 14 | box-shadow: 2px 2px 5px rgba(0,0,0,0.5); 15 | // display: none; 16 | 17 | @include transition(width 0.5s ease, height 0.5s ease, opacity 0.5s ease, left 0.5s ease, top 0.5s ease); 18 | 19 | & > img { 20 | width: 100%; 21 | } 22 | 23 | &.active { 24 | // display: block; 25 | width: 90%; 26 | height: 90%; 27 | left: 5%; 28 | top: 5%; 29 | opacity: 1; 30 | cursor: pointer; 31 | pointer-events: all; 32 | } 33 | 34 | .credit { 35 | position: absolute; 36 | width: 100%; 37 | bottom: -2px; 38 | background: rgba(0,0,0,0.5); 39 | 40 | & > div { 41 | margin: 10px 20px; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/js/widgets/Timetable/Timetable.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Esri 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | import { subclass, property } from "esri/core/accessorSupport/decorators"; 18 | import { tsx } from "esri/widgets/support/widget"; 19 | import Widget from "esri/widgets/Widget"; 20 | import Collection from "esri/core/Collection"; 21 | 22 | const daysName = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; 23 | 24 | interface TimetableCtorArgs { 25 | dates: Collection; 26 | } 27 | 28 | interface DayTimetableCtorArgs { 29 | opens: string; 30 | closes: string; 31 | } 32 | 33 | @subclass("widgets/Timetable") 34 | export class DayTimetable extends Widget { 35 | @property() 36 | opens: string; 37 | 38 | @property() 39 | closes: string; 40 | 41 | @property() 42 | index = 0; 43 | 44 | @property({dependsOn: ["opens", "index"]}) 45 | private get openDate() { 46 | if (!this.opens) { 47 | return new Date(); 48 | } 49 | 50 | const time = this.opens.split(":").map((aTime) => parseInt(aTime)); 51 | return new Date(2019, 2, 18 + this.index, time[0], time[1]); 52 | } 53 | 54 | @property({dependsOn: ["closes", "index"]}) 55 | private get closeDate() { 56 | if (!this.closes) { 57 | return new Date(); 58 | } 59 | 60 | const time = this.closes.split(":").map((aTime) => parseInt(aTime)); 61 | return new Date(2019, 2, 18 + this.index, time[0], time[1]); 62 | } 63 | 64 | render() { 65 | const today = new Date(); 66 | if (today.getDay() === this.openDate.getDay()) { 67 | return (
    68 |

    Today

    69 |
    {this.openDate.getHours()}:00 - {this.closeDate.getHours()}:00
    70 |
    ); 71 | } 72 | else { 73 | return (
    74 |

    {daysName[this.openDate.getDay()]}

    75 |
    {this.openDate.getHours()}:00 - {this.closeDate.getHours()}:00
    76 |
    ); 77 | } 78 | } 79 | 80 | constructor(args: DayTimetableCtorArgs) { 81 | super(args as any); 82 | } 83 | } 84 | 85 | @subclass("widgets/Timetable") 86 | export class Timetable extends Widget { 87 | @property() 88 | today = new Date(); 89 | 90 | @property() 91 | dates: Collection; 92 | 93 | render() { 94 | const dates = this.dates.map(d => d.render()).toArray(); 95 | return (
    {dates}
    ); 96 | } 97 | 98 | constructor(args: TimetableCtorArgs) { 99 | super(args as any); 100 | 101 | args.dates.forEach((date, i) => date.index = i); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/js/widgets/Timetable/css/timetable.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../css/variables.scss"; 2 | @import "../../../../css/mixins.scss"; 3 | 4 | .timetable { 5 | width: 200px; 6 | margin-top: -10px; 7 | pointer-events: all; 8 | overflow: hidden; 9 | 10 | .daytime { 11 | @include transition(margin 0.3s ease); 12 | margin-left: -200px; 13 | clear: both; 14 | width: 100%; 15 | height: 30px; 16 | 17 | h3 { 18 | float: left; 19 | display: inline-block; 20 | font-size: 18px; 21 | } 22 | 23 | .schedule, .openstatus { 24 | float: right; 25 | display: inline-block; 26 | font-size: 90%; 27 | margin-top: 3px; 28 | } 29 | 30 | &.today { 31 | color: $orange; 32 | font-weight: bold; 33 | } 34 | } 35 | } 36 | 37 | .active .timetable { 38 | .daytime { 39 | margin-left: 0; 40 | } 41 | 42 | .daytime:nth-child(1) { 43 | @include transition-delay(0.1s); 44 | } 45 | 46 | .daytime:nth-child(2) { 47 | @include transition-delay(0.2s); 48 | } 49 | 50 | .daytime:nth-child(3) { 51 | @include transition-delay(0.3s); 52 | } 53 | 54 | .daytime:nth-child(4) { 55 | @include transition-delay(0.4s); 56 | } 57 | 58 | .daytime:nth-child(5) { 59 | @include transition-delay(0.5s); 60 | } 61 | 62 | .daytime:nth-child(6) { 63 | @include transition-delay(0.6s); 64 | } 65 | 66 | .daytime:nth-child(7) { 67 | @include transition-delay(0.7s); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/js/widgets/Toggle/Toggle.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Esri 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | import { subclass, property } from "esri/core/accessorSupport/decorators"; 18 | import { tsx } from "esri/widgets/support/widget"; 19 | import Widget from "esri/widgets/Widget"; 20 | 21 | @subclass("widgets/Toggle") 22 | class Toggle extends Widget { 23 | @property() 24 | active: boolean = false; 25 | 26 | render() { 27 | const activeClass = { 28 | "active": this.active 29 | }; 30 | const knob = (
    ); 31 | return (
    {knob}
    ); 32 | } 33 | 34 | private onClick(event: Event) { 35 | event.stopPropagation(); 36 | this.active = (!this.active); 37 | } 38 | } 39 | 40 | export = Toggle; 41 | -------------------------------------------------------------------------------- /src/js/widgets/Toggle/css/toggle.scss: -------------------------------------------------------------------------------- 1 | @use "sass:math"; 2 | @import "../../../../css/variables.scss"; 3 | @import "../../../../css/mixins.scss"; 4 | 5 | $color: #a3a3a3; 6 | $size: 16px; 7 | 8 | .toggle { 9 | display: inline-block; 10 | width: 35px; 11 | height: $size; 12 | border-radius: $size; 13 | border: solid 1px $primaryColor; 14 | background: rgba(100,100,100,0.5); 15 | cursor: pointer; 16 | 17 | .knob { 18 | height: ($size - 2); 19 | width: ($size - 2); 20 | border-radius: math.div($size - 2, 2); 21 | margin-top: 1px; 22 | margin-left: 1px; 23 | background-color: $primaryColor; 24 | 25 | @include transition(all 0.3s); 26 | } 27 | 28 | &.active { 29 | border-color: $orange; 30 | 31 | .knob { 32 | margin-left: 19px; 33 | background-color: $orange; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/js/widgets/Viewpoints/OneViewpoint.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Esri 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | import { subclass, property } from "esri/core/accessorSupport/decorators"; 18 | import { tsx } from "esri/widgets/support/widget"; 19 | import Slide from "esri/webscene/Slide"; 20 | import AppState from "../../AppState"; 21 | import Widget from "esri/widgets/Widget"; 22 | 23 | @subclass("widgets/Viewpoints/Viewpoint") 24 | class OneViewpoint extends Widget { 25 | @property() 26 | slide: Slide; 27 | 28 | @property() 29 | appState: AppState; 30 | 31 | @property() 32 | active: boolean = false; 33 | 34 | render() { 35 | const activeClass = { 36 | "active": this.active 37 | }; 38 | return (
  • 39 | {this.slide.title.text} 40 |
  • ); 41 | } 42 | 43 | constructor(args: any) { 44 | super(args); 45 | } 46 | 47 | private onClick() { 48 | event.stopPropagation(); 49 | this.active = true; 50 | this.appState.view.goTo(this.slide.viewpoint); 51 | } 52 | } 53 | 54 | export = OneViewpoint; 55 | -------------------------------------------------------------------------------- /src/js/widgets/Viewpoints/Viewpoints.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Esri 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | import { subclass, property } from "esri/core/accessorSupport/decorators"; 18 | import { tsx } from "esri/widgets/support/widget"; 19 | import AppState from "../../AppState"; 20 | import Widget from "esri/widgets/Widget"; 21 | import WebScene from "esri/WebScene"; 22 | import Collection from "esri/core/Collection"; 23 | import * as watchUtils from "esri/core/watchUtils"; 24 | import Handles from "esri/core/Handles"; 25 | import OneViewpoint from "./OneViewpoint"; 26 | 27 | @subclass("widgets/Viewpoints") 28 | class Viewpoints extends Widget { 29 | @property() 30 | appState: AppState; 31 | 32 | private handles = new Handles(); 33 | 34 | @property() 35 | set activeViewpoint(viewpointToActivate: OneViewpoint) { 36 | this.slides.forEach(viewpoint => { 37 | if (viewpoint !== viewpointToActivate) { 38 | viewpoint.active = false; 39 | } 40 | else { 41 | viewpoint.active = true; 42 | } 43 | }); 44 | this._set("activeViewpoint", viewpointToActivate); 45 | } 46 | 47 | @property({readOnly: true, dependsOn: ["appState.view.map.presentation.slides"]}) 48 | get slides(): Collection { 49 | return this.appState ? 50 | (this.appState.view.map as WebScene).presentation.slides 51 | .map((s) => new OneViewpoint({slide: s, appState: this.appState})) 52 | : new Collection(); 53 | } 54 | 55 | constructor(args: any) { 56 | super(args); 57 | } 58 | 59 | render() { 60 | const items = this.slides.length > 0 ? this.slides.map((s) => s.render()).toArray() : null; 61 | return this.slides.length > 0 ? (
    62 |

    Point of view

    63 |
      64 | {items} 65 |
    66 |
    ) : null; 67 | } 68 | 69 | postInitialize() { 70 | (this.appState.view.map as WebScene).presentation.slides.on("change", () => this.notifyChange("slides")); 71 | this.slides.on("change", this.watchActiveSlide.bind(this)); 72 | this.watchActiveSlide(); 73 | } 74 | 75 | watchActiveSlide() { 76 | this.handles.removeAll(); 77 | this.slides.forEach(s => { 78 | this.handles.add( 79 | watchUtils.init(s, "active", (active) => { 80 | if (active) { 81 | this.activeViewpoint = s; 82 | } 83 | }), 84 | "active" 85 | ); 86 | }); 87 | } 88 | } 89 | 90 | export = Viewpoints; 91 | -------------------------------------------------------------------------------- /src/js/widgets/Viewpoints/css/viewpoints.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../css/variables.scss"; 2 | @import "../../../../css/mixins.scss"; 3 | 4 | #appDiv .viewpoints { 5 | width: 300px; 6 | margin-left: -100px; 7 | pointer-events: all; 8 | 9 | h2.slash-title { 10 | margin-bottom: 20px; 11 | } 12 | 13 | & > div > div { 14 | height: 30px; 15 | } 16 | 17 | a { 18 | color: $viewpointItemColor; 19 | margin-left: 8px; 20 | text-decoration: none; 21 | } 22 | 23 | svg { 24 | width: 15px; 25 | opacity: 0.5; 26 | vertical-align: -2px; 27 | } 28 | 29 | .viewpoint { 30 | @include transition(all 0.5s); 31 | margin-left: 200px; 32 | font-family: 'Roboto Condensed', sans-serif; // import font! 33 | cursor: pointer; 34 | list-style: none; 35 | display: block; 36 | pointer-events: all; 37 | width: 100%; 38 | font-size: $viewpointItemFontSize; 39 | text-align: right; 40 | 41 | &:first-child { 42 | margin-top: 0; 43 | } 44 | 45 | &.active { 46 | font-size: $viewpointItemActiveFontSize; 47 | color: $viewpointItemActiveColor; 48 | 49 | &:first-child { 50 | margin-top: 0; 51 | } 52 | 53 | &:hover { 54 | color: $viewpointItemActiveColor; 55 | } 56 | } 57 | 58 | &:hover { 59 | font-size: $viewpointItemOverFontSize; 60 | color: $viewpointItemOverColor; 61 | } 62 | } 63 | } 64 | 65 | 66 | #appDiv .active .viewpoints { 67 | .viewpoint { 68 | margin-left: 0; 69 | } 70 | 71 | .viewpoint:nth-child(1) { 72 | @include transition(all 0.5s, margin-left 0.8s 0.1s); 73 | } 74 | 75 | .viewpoint:nth-child(2) { 76 | @include transition(all 0.5s, margin-left 0.8s 0.2s); 77 | } 78 | 79 | .viewpoint:nth-child(3) { 80 | @include transition(all 0.5s, margin-left 0.8s 0.3s); 81 | } 82 | 83 | .viewpoint:nth-child(4) { 84 | @include transition(all 0.5s, margin-left 0.8s 0.4s); 85 | } 86 | 87 | .viewpoint:nth-child(5) { 88 | @include transition(all 0.5s, margin-left 0.8s 0.5s); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/js/widgets/widgets.scss: -------------------------------------------------------------------------------- 1 | @import url("./Toggle/css/toggle.css"); 2 | @import url("./FloorSelector/css/floorSelector.css"); 3 | @import url("./Timetable/css/timetable.css"); 4 | @import url("./Viewpoints/css/viewpoints.css"); 5 | @import url("./Popup/css/popup.css"); 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "experimentalDecorators": true, 5 | "inlineSourceMap": false, 6 | "jsx": "react", 7 | "lib": [ "dom", "es2015", "scripthost" ], 8 | "module": "amd", 9 | "importHelpers": true, 10 | "noEmitHelpers": true, 11 | "noEmitOnError": true, 12 | "noImplicitAny": true, 13 | "noImplicitThis": true, 14 | "noImplicitUseStrict": true, 15 | "noUnusedLocals": true, 16 | "jsxFactory": "tsx", 17 | "suppressImplicitAnyIndexErrors": true, 18 | "target": "es5", 19 | "baseUrl": ".", 20 | "moduleResolution": "node", 21 | "types": [ 22 | "dojo", 23 | "./typings/arcgis-js-api-4.20" 24 | ] 25 | }, 26 | "include": [ 27 | "./src/js/**/*" 28 | ], 29 | "exclude": [ 30 | "node_modules", 31 | "typings" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "align": false, 4 | "ban": false, 5 | "class-name": true, 6 | "comment-format": false, 7 | "curly": true, 8 | "eofline": true, 9 | "forin": false, 10 | "indent": [ 11 | true, 12 | "spaces" 13 | ], 14 | "interface-name": false, 15 | "jsdoc-format": true, 16 | "label-position": true, 17 | "max-line-length": false, 18 | "member-access": false, 19 | "member-ordering": false, 20 | "no-any": false, 21 | "no-arg": true, 22 | "no-bitwise": false, 23 | "no-conditional-assignment": false, 24 | "no-console": false, 25 | "no-consecutive-blank-lines": false, 26 | "no-construct": true, 27 | "no-debugger": true, 28 | "no-default-export": false, 29 | "no-duplicate-variable": true, 30 | "no-empty": false, 31 | "no-eval": true, 32 | "no-inferrable-types": true, 33 | "no-internal-module": true, 34 | "no-require-imports": false, 35 | "no-shadowed-variable": false, 36 | "no-string-literal": false, 37 | "no-switch-case-fall-through": false, 38 | "no-trailing-whitespace": false, 39 | "no-unused-expression": false, 40 | "no-use-before-declare": false, 41 | "no-var-keyword": true, 42 | "no-var-requires": false, 43 | "one-line": [ 44 | true, 45 | "check-open-brace", 46 | "check-whitespace" 47 | ], 48 | "quotemark": [ 49 | true, 50 | "double" 51 | ], 52 | "radix": true, 53 | "semicolon": [ 54 | true, 55 | "always" 56 | ], 57 | "switch-default": false, 58 | "trailing-comma": [ 59 | true, 60 | { 61 | "multiline": "never", 62 | "singleline": "never" 63 | } 64 | ], 65 | "triple-equals": [ 66 | true, 67 | "allow-null-check" 68 | ], 69 | "typedef": false, 70 | "typedef-whitespace": [ 71 | true, 72 | { 73 | "call-signature": "nospace", 74 | "index-signature": "nospace", 75 | "parameter": "nospace", 76 | "property-declaration": "nospace", 77 | "variable-declaration": "nospace" 78 | }, 79 | { 80 | "call-signature": "onespace", 81 | "index-signature": "onespace", 82 | "parameter": "onespace", 83 | "property-declaration": "onespace", 84 | "variable-declaration": "onespace" 85 | } 86 | ], 87 | "variable-name": false, 88 | "whitespace": [ 89 | true, 90 | "check-branch", 91 | "check-decl", 92 | "check-operator", 93 | "check-module", 94 | "check-separator", 95 | "check-type", 96 | "check-typecast" 97 | ] 98 | } 99 | } --------------------------------------------------------------------------------