├── src
├── assets
│ ├── .gitkeep
│ ├── ic_logo.png
│ ├── ic_logo@2X.png
│ └── basemap.json
├── app
│ ├── map
│ │ ├── map.component.html
│ │ ├── map.component.css
│ │ └── map.component.ts
│ ├── app.component.html
│ ├── app.component.css
│ ├── terms
│ │ ├── terms.component.css
│ │ ├── terms.component.html
│ │ └── terms.component.ts
│ ├── services
│ │ ├── geojson.service.ts
│ │ ├── firestore.service.ts
│ │ ├── styles.service.ts
│ │ └── bigquery.service.ts
│ ├── app.component.ts
│ ├── rule
│ │ ├── rule.component.css
│ │ ├── rule.component.html
│ │ └── rule.component.ts
│ ├── file-size.pipe.ts
│ ├── app.routing.ts
│ ├── app.component.spec.ts
│ ├── app.constants.ts
│ ├── main
│ │ ├── main.component.css
│ │ ├── main.component.html
│ │ └── main.component.ts
│ └── app.module.ts
├── favicon.ico
├── tsconfig.app.json
├── styles.css
├── tsconfig.spec.json
├── main.ts
├── typings.d.ts
├── environments
│ ├── environment.prod.ts
│ └── environment.ts
├── polyfills.ts
├── index.html
├── test.ts
└── theme.scss
├── preview.png
├── assets
├── sql.png
├── hero.png
├── schema.png
├── permissions.png
├── style_fillColor.png
├── permissions_prompt.png
├── style_circleRadius.png
└── style_fillOpacity.png
├── .editorconfig
├── tsconfig.json
├── .gitignore
├── server.js
├── third_party
└── ng2-codemirror
│ ├── LICENSE
│ ├── codemirror.component.css
│ └── codemirror.component.ts
├── CONTRIBUTING.md
├── app.yaml
├── README.md
├── karma.conf.js
├── package.json
├── tslint.json
├── angular.json
└── LICENSE
/src/assets/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/map/map.component.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/donmccurdy/bigquery-geo-viz/master/preview.png
--------------------------------------------------------------------------------
/src/app/app.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/assets/sql.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/donmccurdy/bigquery-geo-viz/master/assets/sql.png
--------------------------------------------------------------------------------
/assets/hero.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/donmccurdy/bigquery-geo-viz/master/assets/hero.png
--------------------------------------------------------------------------------
/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/donmccurdy/bigquery-geo-viz/master/src/favicon.ico
--------------------------------------------------------------------------------
/assets/schema.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/donmccurdy/bigquery-geo-viz/master/assets/schema.png
--------------------------------------------------------------------------------
/assets/permissions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/donmccurdy/bigquery-geo-viz/master/assets/permissions.png
--------------------------------------------------------------------------------
/src/assets/ic_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/donmccurdy/bigquery-geo-viz/master/src/assets/ic_logo.png
--------------------------------------------------------------------------------
/assets/style_fillColor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/donmccurdy/bigquery-geo-viz/master/assets/style_fillColor.png
--------------------------------------------------------------------------------
/src/assets/ic_logo@2X.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/donmccurdy/bigquery-geo-viz/master/src/assets/ic_logo@2X.png
--------------------------------------------------------------------------------
/assets/permissions_prompt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/donmccurdy/bigquery-geo-viz/master/assets/permissions_prompt.png
--------------------------------------------------------------------------------
/assets/style_circleRadius.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/donmccurdy/bigquery-geo-viz/master/assets/style_circleRadius.png
--------------------------------------------------------------------------------
/assets/style_fillOpacity.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/donmccurdy/bigquery-geo-viz/master/assets/style_fillOpacity.png
--------------------------------------------------------------------------------
/src/app/app.component.css:
--------------------------------------------------------------------------------
1 | main {
2 | min-height: 100vh;
3 | display: flex;
4 | flex-direction: column;
5 | }
6 |
--------------------------------------------------------------------------------
/src/app/map/map.component.css:
--------------------------------------------------------------------------------
1 | :host,
2 | .map {
3 | width: 100%;
4 | height: 100%;
5 | min-width: 45vw;
6 | min-height: calc(100vh - 64px);
7 | }
8 |
--------------------------------------------------------------------------------
/src/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "allowJs": true,
5 | "outDir": "../out-tsc/app",
6 | "baseUrl": "./",
7 | "module": "es2015"
8 | },
9 | "exclude": [
10 | "test.ts",
11 | "**/*.spec.ts"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | max_line_length = off
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/src/styles.css:
--------------------------------------------------------------------------------
1 | /* You can add global styles to this file, and also import other style files */
2 |
3 | html, body {
4 | margin: 0;
5 | padding: 0;
6 | }
7 |
8 | color-picker .color-picker {
9 | border: none;
10 | box-shadow: 1px 0px 4px 2px rgba(0,0,0,0.15);
11 | border-radius: 2px;
12 | }
13 |
--------------------------------------------------------------------------------
/src/app/terms/terms.component.css:
--------------------------------------------------------------------------------
1 | .view {
2 | font-family: Roboto, Helvetica, sans-serif;
3 | padding: 1em;
4 | max-width: 700px;
5 | }
6 |
7 | .flex-spacer {
8 | flex-grow: 1;
9 | }
10 |
11 | nav {
12 | text-align: center;
13 | }
14 |
15 | .back {
16 | margin-top: 2em;
17 | }
18 |
19 | a.policy-link {
20 | color: #4285f4;
21 | font-weight: 400;
22 | }
23 |
--------------------------------------------------------------------------------
/src/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/spec",
5 | "baseUrl": "./",
6 | "module": "commonjs",
7 | "target": "es5",
8 | "types": [
9 | "jasmine",
10 | "node"
11 | ]
12 | },
13 | "files": [
14 | "test.ts",
15 | "polyfills.ts"
16 | ],
17 | "include": [
18 | "**/*.spec.ts",
19 | "**/*.d.ts"
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "outDir": "./dist/out-tsc",
5 | "sourceMap": true,
6 | "declaration": false,
7 | "moduleResolution": "node",
8 | "emitDecoratorMetadata": true,
9 | "experimentalDecorators": true,
10 | "target": "es5",
11 | "typeRoots": [
12 | "node_modules/@types"
13 | ],
14 | "lib": [
15 | "es2017",
16 | "dom"
17 | ],
18 | "module": "es2015",
19 | "baseUrl": "./"
20 | }
21 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /tmp
6 | /out-tsc
7 |
8 | # dependencies
9 | /node_modules
10 |
11 | # IDEs and editors
12 | /.idea
13 | .project
14 | .classpath
15 | .c9/
16 | *.launch
17 | .settings/
18 | *.sublime-workspace
19 |
20 | # IDE - VSCode
21 | .vscode/*
22 | !.vscode/settings.json
23 | !.vscode/tasks.json
24 | !.vscode/launch.json
25 | !.vscode/extensions.json
26 |
27 | # misc
28 | /.sass-cache
29 | /connect.lock
30 | /coverage
31 | /libpeerconnection.log
32 | npm-debug.log
33 | testem.log
34 | /typings
35 |
36 | # e2e
37 | /e2e/*.js
38 | /e2e/*.map
39 |
40 | # System Files
41 | .DS_Store
42 | Thumbs.db
43 |
44 | # Dev data
45 | assets/tmp/*
46 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
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 | * https://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 | const serve = require('serve');
18 | const server = serve('./dist', {port: process.env.PORT});
19 |
--------------------------------------------------------------------------------
/src/app/services/geojson.service.ts:
--------------------------------------------------------------------------------
1 | export interface GeoJSONFeature {
2 | geometry: {};
3 | properties: object;
4 | }
5 |
6 | export class GeoJSONService {
7 | /**
8 | * Converts rows to GeoJSON features.
9 | * @param rows
10 | * @param geoColumn
11 | */
12 | static rowsToGeoJSON(rows: object[], geoColumn: string): GeoJSONFeature[] {
13 | if (!rows || !geoColumn) return [];
14 |
15 | // Convert rows to GeoJSON features.
16 | const features = [];
17 | rows.forEach((row) => {
18 | if (!row[geoColumn]) return;
19 | try {
20 | const geometry = JSON.parse(row[geoColumn]);
21 | const feature = { type: 'Feature', geometry, properties: row };
22 | features.push(feature);
23 | } catch (e) {
24 | // Parsing can fail (e.g. invalid GeoJSON); just log the error.
25 | console.error(e);
26 | }
27 | });
28 |
29 | return features;
30 | }
31 | }
--------------------------------------------------------------------------------
/src/app/terms/terms.component.html:
--------------------------------------------------------------------------------
1 |
6 |
7 |
Terms of Service & Privacy
8 |
9 | This tool is provided as a reference implementation of Google Maps and geospatial BigQuery API usage capabilities. It may
10 | be useful as a debugging and visualization resource. It is not an officially supported Google product and is provided without
11 | guarantees of maintenance.
12 |
13 |
14 | Google Terms of Service
15 | Google Cloud Platform Terms of Service
16 | Privacy
17 |
18 |
Back
19 |
20 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
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 | * https://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 { enableProdMode } from '@angular/core';
18 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
19 |
20 | import { AppModule } from './app/app.module';
21 | import { environment } from './environments/environment';
22 |
23 | if (environment.production) {
24 | enableProdMode();
25 | }
26 |
27 | platformBrowserDynamic().bootstrapModule(AppModule)
28 | .catch(err => console.log(err));
29 |
--------------------------------------------------------------------------------
/src/typings.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
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 | * https://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 | /* SystemJS module definition */
18 | declare var module: NodeModule;
19 | interface NodeModule {
20 | id: string;
21 | }
22 |
23 | /* Global promises resolved when Google libraries are available */
24 | declare var pendingMap: Promise;
25 | declare var pendingGapi: Promise;
26 |
27 | /* Google Analytics */
28 | declare var gtag: (method: string, action: string, detail: Object) => undefined;
29 |
--------------------------------------------------------------------------------
/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
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 | * https://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 { Component, ViewContainerRef } from '@angular/core';
18 | import 'rxjs/add/operator/debounceTime';
19 | import 'rxjs/add/operator/map';
20 |
21 | @Component({
22 | selector: 'app-root',
23 | templateUrl: './app.component.html',
24 | styleUrls: ['./app.component.css']
25 | })
26 | export class AppComponent {
27 | readonly title = 'BigQuery Geo Viz';
28 | constructor(public viewContainerRef: ViewContainerRef) {}
29 | }
30 |
--------------------------------------------------------------------------------
/src/app/terms/terms.component.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
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 | * https://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 { Component, AfterViewInit } from '@angular/core';
18 |
19 | @Component({
20 | selector: 'app-terms',
21 | templateUrl: './terms.component.html',
22 | styleUrls: ['./terms.component.css']
23 | })
24 | export class TermsComponent implements AfterViewInit {
25 | constructor() {}
26 |
27 | /**
28 | * Constructs a Maps API instance after DOM has initialized.
29 | */
30 | ngAfterViewInit() {
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/app/rule/rule.component.css:
--------------------------------------------------------------------------------
1 | mat-slide-toggle {
2 | margin: 1em 0;
3 | }
4 |
5 | mat-form-field {
6 | display: block;
7 | max-width: 250px;
8 | }
9 |
10 | .array-field + .array-field {
11 | margin-top: 1em;
12 | }
13 |
14 | .array-field-list {
15 | display: block;
16 | }
17 |
18 | .array-field-item {
19 | position: relative;
20 | }
21 |
22 | .array-field-input {
23 | border: 1px solid rgba(0, 0, 0, 0.32);
24 | height: 32px;
25 | line-height: 32px;
26 | display: inline-block;
27 | width: 72px;
28 | margin: 0.5em 0.5em 0.5em 0;
29 | padding: 0 1em;
30 | font-family: inherit;
31 | font-size: 1em;
32 | color: rgba(0, 0, 0, 0.54);
33 | }
34 |
35 | .array-field-caption {
36 | position: absolute;
37 | bottom: -1.2em;
38 | font-size: 0.8em;
39 | color: #888;
40 | }
41 |
42 | .array-field-swatch {
43 | display: inline-block;
44 | width: 20px;
45 | height: 20px;
46 | border-radius: 2px;
47 | position: absolute;
48 | right: 1em;
49 | top: 1em;
50 | }
51 |
52 | button:not([disabled]) mat-icon.create {
53 | color: #33AC71;
54 | }
55 |
--------------------------------------------------------------------------------
/third_party/ng2-codemirror/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Simon Babay and Google
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to Contribute
2 |
3 | We'd love to accept your patches and contributions to this project. There are
4 | just a few small guidelines you need to follow.
5 |
6 | ## Contributor License Agreement
7 |
8 | Contributions to this project must be accompanied by a Contributor License
9 | Agreement. You (or your employer) retain the copyright to your contribution;
10 | this simply gives us permission to use and redistribute your contributions as
11 | part of the project. Head over to to see
12 | your current agreements on file or to sign a new one.
13 |
14 | You generally only need to submit a CLA once, so if you've already submitted one
15 | (even if it was for a different project), you probably don't need to do it
16 | again.
17 |
18 | ## Code reviews
19 |
20 | All submissions, including submissions by project members, require review. We
21 | use GitHub pull requests for this purpose. Consult
22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
23 | information on using pull requests.
24 |
25 | ## Community Guidelines
26 |
27 | This project follows [Google's Open Source Community
28 | Guidelines](https://opensource.google.com/conduct/).
29 |
--------------------------------------------------------------------------------
/app.yaml:
--------------------------------------------------------------------------------
1 | # Copyright 2018 Google LLC
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | runtime: python38
16 |
17 | handlers:
18 | - url: /
19 | secure: always
20 | static_files: index.html
21 | upload: index.html
22 | http_headers:
23 | X-Frame-Options: DENY
24 |
25 | - url: /project/(.*)
26 | secure: always
27 | static_files: index.html
28 | upload: index.html
29 | http_headers:
30 | X-Frame-Options: DENY
31 |
32 | - url: /terms
33 | secure: always
34 | static_files: index.html
35 | upload: index.html
36 | http_headers:
37 | X-Frame-Options: DENY
38 |
39 | - url: /(.*)
40 | secure: always
41 | static_files: \1
42 | upload: (.*)
43 |
--------------------------------------------------------------------------------
/src/app/file-size.pipe.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
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 | * https://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 {Pipe, PipeTransform} from '@angular/core';
18 |
19 | const UNITS = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
20 |
21 | @Pipe({name: 'fileSize'})
22 | export class FileSizePipe implements PipeTransform {
23 |
24 | transform(bytes: number = 0, precision: number = 2): string {
25 | if (!isFinite(bytes)) { return ''; }
26 |
27 | let unit;
28 | for (unit = 0; bytes >= 1024; unit++) {
29 | bytes /= 1024;
30 | }
31 |
32 | const value = bytes.toFixed(Number(precision));
33 | return `${value} ${UNITS[unit]}`;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
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 | * https://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 | export const environment = {
18 | production: true,
19 | authClientID: '419125973937-kl2cru5pu2vfugne7lr1hosgseh4lo1s.apps.googleusercontent.com',
20 | authScope: 'https://www.googleapis.com/auth/bigquery'
21 | };
22 |
23 | // Your web app's Firebase configuration
24 | var firebaseConfig = {
25 | apiKey: "AIzaSyDS8k-x7L9vZ_mvvdyTzwQ1LNXsYLNnhOM",
26 | authDomain: "bigquerygeoviz.firebaseapp.com",
27 | databaseURL: "https://bigquerygeoviz.firebaseio.com",
28 | projectId: "bigquerygeoviz",
29 | storageBucket: "bigquerygeoviz.appspot.com",
30 | messagingSenderId: "419125973937",
31 | appId: "1:419125973937:web:eba1c63d64b58be3ec2390",
32 | measurementId: "G-FNH2K1BP5G"
33 | };
34 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # BigQuery Geo Viz
2 |
3 | Web tool and developer example for visualization of Google BigQuery geospatial data using Google Maps Platform APIs.
4 |
5 | 
6 |
7 | ## Getting started
8 |
9 | - Tool: https://bigquerygeoviz.appspot.com/
10 | - Documentation: https://cloud.google.com/bigquery/docs/gis-analyst-start
11 |
12 | ## Development
13 |
14 | ### Quickstart
15 |
16 | ```shell
17 | # Start a dev server at http://localhost:4200/.
18 | npm run dev
19 |
20 | # Run unit tests with Karma.
21 | npm test
22 | ```
23 | ### Resources
24 |
25 | - [Google Maps JavaScript API documentation](https://developers.google.com/maps/documentation/javascript/)
26 | - [Google BigQuery REST API documentation](https://cloud.google.com/bigquery/docs/reference/rest/v2/)
27 | - [Angular](https://angular.io/)
28 | - [D3.js](https://d3js.org/)
29 | - [TypeScript](https://www.typescriptlang.org/)
30 |
31 | ## Terms & privacy
32 |
33 | This tool is provided as a reference implementation of Google Maps and geospatial BigQuery API usage capabilities. It may
34 | be useful as a debugging and visualization resource. It is not an officially supported Google product and is provided without
35 | guarantees of maintenance.
36 |
37 | - [Google Terms of Service](https://policies.google.com/terms)
38 | - [Google Cloud Platform Terms of Service](https://cloud.google.com/terms/)
39 | - [Privacy](https://policies.google.com/privacy)
40 |
--------------------------------------------------------------------------------
/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
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 | * https://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 | /***************************************************************************************************
18 | * BROWSER POLYFILLS
19 | */
20 |
21 | /** Evergreen browsers require these. **/
22 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
23 |
24 |
25 | /***************************************************************************************************
26 | * Zone JS is required by Angular itself.
27 | */
28 | import 'zone.js/dist/zone'; // Included with Angular CLI.
29 |
30 | /***************************************************************************************************
31 | * APPLICATION IMPORTS
32 | */
33 | // See https://github.com/valor-software/ng2-dragula/issues/849.
34 | (window as any).global = window;
35 |
--------------------------------------------------------------------------------
/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
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 | * https://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 | export const environment = {
18 | production: false,
19 | authClientID: '419125973937-kl2cru5pu2vfugne7lr1hosgseh4lo1s.apps.googleusercontent.com',
20 | authScope: 'https://www.googleapis.com/auth/bigquery'
21 | };
22 |
23 | // Your web app's Firebase configuration
24 | // TODO(hormati): Create a different config for testing.
25 | export const firebaseConfig = {
26 | apiKey: "AIzaSyDS8k-x7L9vZ_mvvdyTzwQ1LNXsYLNnhOM",
27 | authDomain: "bigquerygeoviz.firebaseapp.com",
28 | databaseURL: "https://bigquerygeoviz.firebaseio.com",
29 | projectId: "bigquerygeoviz",
30 | storageBucket: "bigquerygeoviz.appspot.com",
31 | messagingSenderId: "419125973937",
32 | appId: "1:419125973937:web:eba1c63d64b58be3ec2390",
33 | measurementId: "G-FNH2K1BP5G"
34 | };
35 |
--------------------------------------------------------------------------------
/src/app/app.routing.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
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 | * https://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 { Routes } from '@angular/router';
18 | import { TermsComponent } from './terms/terms.component';
19 | import { MainComponent } from './main/main.component';
20 | export const routes: Routes = [
21 | {
22 | path: '',
23 | component: MainComponent
24 | },
25 | {
26 | path: 'terms',
27 | component: TermsComponent
28 | },
29 | {
30 | path: 'project',
31 | children: [
32 | {
33 | path: ':project/dataset/:dataset/table/:table',
34 | component: MainComponent
35 | },
36 | {
37 | path: ':project/job',
38 | children: [
39 | {
40 | path: ':job/location/:location',
41 | component: MainComponent
42 | },
43 | {
44 | path: ':job',
45 | component: MainComponent
46 | },
47 | ]
48 | },
49 | ]
50 | },
51 | ];
52 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | BigQuery Geo Viz
6 |
7 |
8 |
9 |
10 |
11 |
12 |
22 |
23 |
24 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
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 | * https://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 | // Karma configuration file, see link for more information
18 | // https://karma-runner.github.io/1.0/config/configuration-file.html
19 |
20 | module.exports = function (config) {
21 | config.set({
22 | basePath: '',
23 | frameworks: ['jasmine', '@angular-devkit/build-angular'],
24 | plugins: [
25 | require('karma-jasmine'),
26 | require('karma-chrome-launcher'),
27 | require('karma-jasmine-html-reporter'),
28 | require('karma-coverage-istanbul-reporter'),
29 | require('@angular-devkit/build-angular/plugins/karma')
30 | ],
31 | client:{
32 | clearContext: false // leave Jasmine Spec Runner output visible in browser
33 | },
34 | coverageIstanbulReporter: {
35 | dir: require('path').join(__dirname, 'coverage'), reports: [ 'html', 'lcovonly' ],
36 | fixWebpackSourcePaths: true
37 | },
38 |
39 | reporters: ['progress', 'kjhtml'],
40 | port: 9876,
41 | colors: true,
42 | logLevel: config.LOG_INFO,
43 | autoWatch: true,
44 | browsers: ['Chrome'],
45 | singleRun: false
46 | });
47 | };
48 |
--------------------------------------------------------------------------------
/src/app/app.component.spec.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
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 | * https://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 { TestBed, async } from '@angular/core/testing';
18 | import { AppComponent } from './app.component';
19 | describe('AppComponent', () => {
20 | beforeEach(async(() => {
21 | TestBed.configureTestingModule({
22 | declarations: [
23 | AppComponent
24 | ],
25 | }).compileComponents();
26 | }));
27 | it('should create the app', async(() => {
28 | const fixture = TestBed.createComponent(AppComponent);
29 | const app = fixture.debugElement.componentInstance;
30 | expect(app).toBeTruthy();
31 | }));
32 | it(`should have as title 'app'`, async(() => {
33 | const fixture = TestBed.createComponent(AppComponent);
34 | const app = fixture.debugElement.componentInstance;
35 | expect(app.title).toEqual('app');
36 | }));
37 | it('should render title in a h1 tag', async(() => {
38 | const fixture = TestBed.createComponent(AppComponent);
39 | fixture.detectChanges();
40 | const compiled = fixture.debugElement.nativeElement;
41 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!');
42 | }));
43 | });
44 |
--------------------------------------------------------------------------------
/src/test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
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 | * https://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 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files
18 |
19 | import 'zone.js/dist/long-stack-trace-zone';
20 | import 'zone.js/dist/proxy.js';
21 | import 'zone.js/dist/sync-test';
22 | import 'zone.js/dist/jasmine-patch';
23 | import 'zone.js/dist/async-test';
24 | import 'zone.js/dist/fake-async-test';
25 | import { getTestBed } from '@angular/core/testing';
26 | import {
27 | BrowserDynamicTestingModule,
28 | platformBrowserDynamicTesting
29 | } from '@angular/platform-browser-dynamic/testing';
30 |
31 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any.
32 | declare const __karma__: any;
33 | declare const require: any;
34 |
35 | // Prevent Karma from running prematurely.
36 | __karma__.loaded = function () {};
37 |
38 | // First, initialize the Angular testing environment.
39 | getTestBed().initTestEnvironment(
40 | BrowserDynamicTestingModule,
41 | platformBrowserDynamicTesting()
42 | );
43 | // Then we find all the tests.
44 | const context = require.context('./', true, /\.spec\.ts$/);
45 | // And load the modules.
46 | context.keys().map(context);
47 | // Finally, start Karma to run the tests.
48 | __karma__.start();
49 |
--------------------------------------------------------------------------------
/src/theme.scss:
--------------------------------------------------------------------------------
1 | @import '~@angular/material/theming';
2 | @include mat-core();
3 |
4 | // primary: #00539b
5 | // accent: #d3e9ef
6 | // white: #ffffff
7 | // grey: #e5e3df
8 |
9 | $primary: (
10 | 50: #e0eaf3,
11 | 100: #b3cbe1,
12 | 200: #80a9cd,
13 | 300: #4d87b9,
14 | 400: #266daa,
15 | 500: #00539b,
16 | 600: #004c93,
17 | 700: #004289,
18 | 800: #00397f,
19 | 900: #00296d,
20 | A100: #d3e9ef,
21 | A200: #d3e9ef,
22 | A400: #d3e9ef,
23 | A700: #d3e9ef,
24 | contrast: (
25 | 50: $black-87-opacity,
26 | 100: $black-87-opacity,
27 | 200: $black-87-opacity,
28 | 300: $black-87-opacity,
29 | 400: $black-87-opacity,
30 | 500: white,
31 | 600: white,
32 | 700: white,
33 | 800: white,
34 | 900: white,
35 | A100: $black-87-opacity,
36 | A200: $black-87-opacity,
37 | A400: $black-87-opacity,
38 | A700: white,
39 | )
40 | );
41 |
42 | $secondary: (
43 | 50: #d3e9ef,
44 | 100: #d3e9ef,
45 | 200: #d3e9ef,
46 | 300: #d3e9ef,
47 | 400: #d3e9ef,
48 | 500: #d3e9ef,
49 | 600: #d3e9ef,
50 | 700: #d3e9ef,
51 | 800: #d3e9ef,
52 | 900: #d3e9ef,
53 | A100: #d3e9ef,
54 | A200: #d3e9ef,
55 | A400: #d3e9ef,
56 | A700: #d3e9ef,
57 | contrast: (
58 | 50: $black-87-opacity,
59 | 100: $black-87-opacity,
60 | 200: $black-87-opacity,
61 | 300: $black-87-opacity,
62 | 400: $black-87-opacity,
63 | 500: white,
64 | 600: white,
65 | 700: white,
66 | 800: white,
67 | 900: white,
68 | A100: $black-87-opacity,
69 | A200: $black-87-opacity,
70 | A400: $black-87-opacity,
71 | A700: white,
72 | )
73 | );
74 |
75 | $bqmapper-primary: mat-palette($primary);
76 | $bqmapper-accent: mat-palette($secondary, 500, 900, A100);
77 | $bqmapper-warn: mat-palette($mat-red);
78 |
79 | $bqmapper-theme: mat-light-theme($bqmapper-primary, $bqmapper-accent, $bqmapper-warn);
80 |
81 | @include angular-material-theme($bqmapper-theme);
82 |
--------------------------------------------------------------------------------
/src/app/app.constants.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
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 | * https://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 * as colorbrewer from 'colorbrewer';
18 |
19 | export const Step = {
20 | DATA: 0,
21 | SCHEMA: 1,
22 | STYLE: 2,
23 | SHARE: 3
24 | };
25 |
26 | // Maximum number of results to be returned by BigQuery API.
27 | export const MAX_RESULTS = 5000000;
28 |
29 | // Maximum number of results to be shown in the HTML preview table.
30 | export const MAX_RESULTS_PREVIEW = 10;
31 |
32 | // How long to wait for the query to complete, in milliseconds, before the request times out and returns.
33 | export const TIMEOUT_MS = 120000;
34 |
35 | // Used to write the sharing data and maintain backward compatibility.
36 | export const SHARING_VERSION = "v1";
37 |
38 | export const SAMPLE_PROJECT_ID = 'google.com:bqmapper';
39 | export const SAMPLE_QUERY = `SELECT
40 | ST_GeogPoint(longitude, latitude) AS WKT,
41 | status,
42 | health,
43 | spc_common,
44 | user_type,
45 | problems,
46 | tree_dbh
47 | FROM \`bigquery-public-data.new_york_trees.tree_census_2015\`
48 | WHERE status = 'Alive'
49 | LIMIT 50000;`;
50 |
51 | // Each page is 10MB. This means the total data will be 250MB at most..
52 | export const MAX_PAGES = 25;
53 |
54 | export const SAMPLE_FILL_OPACITY = {isComputed: false, value: 0.8};
55 | export const SAMPLE_FILL_COLOR = {
56 | isComputed: true,
57 | property: 'health',
58 | function: 'categorical',
59 | domain: ['Poor', 'Fair', 'Good'],
60 | range: ['#F44336', '#FFC107', '#4CAF50']
61 | };
62 | export const SAMPLE_CIRCLE_RADIUS = {
63 | isComputed: true,
64 | property: 'tree_dbh',
65 | function: 'linear',
66 | domain: [0, 500],
67 | range: [10, 50]
68 | };
69 |
70 | export const PALETTES = Object.keys(colorbrewer).map((key) => colorbrewer[key]);
71 |
--------------------------------------------------------------------------------
/src/app/services/firestore.service.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
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 | * https://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 | // Firebase App (the core Firebase SDK) is always required and
18 | // must be listed before other Firebase SDKs
19 | import * as firebase from "firebase/app";
20 |
21 | // Load required services into the firebase namespace.
22 | import "firebase/auth";
23 | import "firebase/firestore";
24 |
25 | import { firebaseConfig } from '../../environments/environment';
26 | import { StyleRule } from '../services/styles.service';
27 |
28 | const SHARING_COLLECTION = 'GeoVizSharing';
29 |
30 | export interface ShareableData {
31 | sharingVersion: string;
32 | projectID : string;
33 | jobID : string;
34 | location : string | undefined;
35 | styles: string;
36 | creationTimestampMs: number;
37 | }
38 |
39 | /**
40 | * Utility class for managing interaction with the Firestore.
41 | */
42 | export class FirestoreService {
43 | private db: firebase.firestore.Firestore = null;
44 |
45 | constructor() {
46 | // Initialize Firebase
47 | firebase.initializeApp(firebaseConfig);
48 | this.db = firebase.firestore();
49 | }
50 |
51 | storeShareableData(shareableData : ShareableData) : Promise {
52 | return this.db.collection(SHARING_COLLECTION).add(shareableData)
53 | .then(function(docRef) {
54 | return docRef.id;
55 | });
56 | }
57 |
58 | getSharedData(docId: string) : Promise {
59 | return this.db.collection(SHARING_COLLECTION).doc(docId).get().then(function(doc) {
60 | if (!doc.exists) {
61 | throw new Error('Shared visualization does not exist. Please check your URL!');
62 | }
63 | return doc.data() as ShareableData;
64 | });
65 | }
66 |
67 | authorize(credential: object) {
68 | const firebase_credential = firebase.auth.GoogleAuthProvider.credential(
69 | credential['id_token'],
70 | credential['access_token']
71 | );
72 | firebase.auth().signInWithCredential(firebase_credential)
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/app/main/main.component.css:
--------------------------------------------------------------------------------
1 | .view {
2 | display: flex;
3 | flex-direction: row-reverse;
4 | }
5 |
6 | .header-profile {
7 | font-size: 14px;
8 | padding: 0 16px;
9 | }
10 |
11 | .header-logo {
12 | height: 32px;
13 | margin-right: 0.5em;
14 | }
15 |
16 | .flex-spacer {
17 | flex-grow: 1;
18 | }
19 |
20 | .toolbar-divider {
21 | padding: 0 16px;
22 | }
23 |
24 | .drawer {
25 | max-width: 600px;
26 | max-height: calc(100vh - 64px);
27 | overflow: auto;
28 | flex-shrink: 0;
29 | }
30 |
31 | .sidenav-container {
32 | position: fixed;
33 | left: 0;
34 | right: 0;
35 | }
36 |
37 | .stepper {
38 | max-width: 100%;
39 | overflow: auto;
40 | flex-shrink: 0;
41 | }
42 |
43 | mat-progress-spinner {
44 | display: inline-block;
45 | vertical-align: middle;
46 | }
47 |
48 | .apply-style-button {
49 | margin-top: 0.5em;
50 | }
51 |
52 | .create-share-link-button {
53 | margin-bottom: 0.5em;
54 | }
55 |
56 | .toggle-button,
57 | .preset-button {
58 | position: fixed;
59 | bottom: 1em;
60 | background: #00539b;
61 | z-index: 10000;
62 | }
63 |
64 | .toggle-button {
65 | left: 1em;
66 | }
67 |
68 | .preset-button {
69 | /* spacing + buttonWidth + spacing */
70 | left: calc(1em + 40px + 1em);
71 | }
72 |
73 | .sql-form-field {
74 | display: block;
75 | width: 100%;
76 | max-width: 600px;
77 | }
78 |
79 | .sql-lint {
80 | font-size: 0.8em;
81 | color: crimson;
82 | }
83 |
84 | .sql-caption {
85 | color: #888;
86 | font-size: 0.8em;
87 | }
88 |
89 | .sql-location {
90 | margin-top: 1em;
91 | display: block;
92 | }
93 |
94 | .num-results-text {
95 | font-size: 0.8em;
96 | }
97 |
98 | .result-table {
99 | overflow-x: auto;
100 | }
101 |
102 | .result-table-cell {
103 | overflow: hidden;
104 | white-space: nowrap;
105 | text-overflow: ellipsis;
106 | }
107 |
108 | .styles-prop-list .rule-badge {
109 | font-size: 0.6em;
110 | font-weight: 500;
111 | display: inline-block;
112 | padding: 0.5em;
113 | color: #ffffff;
114 | border-radius: 2px;
115 | }
116 |
117 | .styles-prop-list mat-panel-title {
118 | width: 150px;
119 | flex-grow: 0;
120 | }
121 |
122 | .styles-prop-list .rule-badge.computed { background: #00539b; }
123 | .styles-prop-list .rule-badge.global { background: #F4B400; }
124 | .styles-prop-list .rule-badge.none { background: #e5e3df; color: #000000; }
125 |
126 | .mat-toolbar.mat-primary,
127 | .mat-raised-button.mat-primary,
128 | .mat-step-header .mat-step-icon {
129 | color: #FFFFFF;
130 | }
131 |
132 | .mat-raised-button[disabled] {
133 | background: #e5e3df;
134 | }
135 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bigquerygeoviz",
3 | "version": "0.0.0",
4 | "license": "MIT",
5 | "scripts": {
6 | "start": "node server.js",
7 | "ng": "ng",
8 | "dev": "ng serve",
9 | "build": "ng build",
10 | "test": "ng test",
11 | "lint": "ng lint",
12 | "predeploy": "npm run build",
13 | "predeploy:beta": "npm run predeploy",
14 | "predeploy:dev": "npm run predeploy",
15 | "deploy": "cp app.yaml dist/app.yaml && cd dist && gcloud app deploy --project bigquerygeoviz --version prod",
16 | "deploy:beta": "cp app.yaml dist/app.yaml && cd dist && gcloud app deploy --project bigquerygeoviz --version beta --no-promote",
17 | "deploy:dev": "cp app.yaml dist/app.yaml && cd dist && gcloud app deploy --project google.com:donmccurdy --version bigquerygeoviz --no-promote"
18 | },
19 | "private": true,
20 | "dependencies": {
21 | "@angular/animations": "^5.0.0",
22 | "@angular/cdk": "^5.0.0-rc0",
23 | "@angular/common": "^5.0.0",
24 | "@angular/compiler": "^5.0.0",
25 | "@angular/core": "^5.0.0",
26 | "@angular/forms": "^5.0.0",
27 | "@angular/http": "^5.0.0",
28 | "@angular/material": "^5.0.0-rc0",
29 | "@angular/platform-browser": "^5.0.0",
30 | "@angular/platform-browser-dynamic": "^5.0.0",
31 | "@angular/router": "^5.0.0",
32 | "@deck.gl/core": "^7.3.15",
33 | "@deck.gl/google-maps": "^7.3.15",
34 | "@deck.gl/layers": "^7.3.15",
35 | "@turf/bbox": "^6.0.1",
36 | "@types/crypto-js": "^3.1.44",
37 | "@types/d3-scale": "^1.0.11",
38 | "@types/gapi": "0.0.35",
39 | "@types/googlemaps": "^3.38.0",
40 | "angular-webstorage-service": "^1.0.2",
41 | "codemirror": "^5.52.2",
42 | "colorbrewer": "^1.1.0",
43 | "core-js": "^2.6.11",
44 | "crypto-js": "^4.0.0",
45 | "d3-color": "^1.4.0",
46 | "d3-scale": "^1.0.7",
47 | "firebase": "^7.13.1",
48 | "js-file-download": "^0.4.11",
49 | "ng2-codemirror": "^1.1.3",
50 | "ngx-color-picker": "^5.3.8",
51 | "rxjs": "^5.5.2",
52 | "serve": "^10.0.2",
53 | "zone.js": "^0.8.14"
54 | },
55 | "devDependencies": {
56 | "@angular-devkit/build-angular": "~0.10.0",
57 | "@angular/cli": "^7.3.10",
58 | "@angular/compiler-cli": "^5.0.0",
59 | "@angular/language-service": "^5.0.0",
60 | "@types/jasmine": "~2.5.53",
61 | "@types/jasminewd2": "^2.0.8",
62 | "@types/node": "^13.9.8",
63 | "codelyzer": "~3.2.0",
64 | "jasmine-core": "~2.6.2",
65 | "jasmine-spec-reporter": "~4.1.0",
66 | "karma": "^3.0.0",
67 | "karma-chrome-launcher": "~2.1.1",
68 | "karma-cli": "~1.0.1",
69 | "karma-coverage-istanbul-reporter": "^1.2.1",
70 | "karma-jasmine": "~1.1.0",
71 | "karma-jasmine-html-reporter": "^0.2.2",
72 | "node-sass": "^4.12.0",
73 | "protractor": "^5.4.3",
74 | "ts-node": "~3.2.0",
75 | "tslint": "~5.7.0",
76 | "typescript": "^3.8.3"
77 | },
78 | "browser": {
79 | "crypto": false
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
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 | * https://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 { BrowserModule } from '@angular/platform-browser';
18 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
19 | import { CodemirrorModule } from 'ng2-codemirror';
20 | import { NgModule, enableProdMode } from '@angular/core';
21 |
22 | import { routes } from './app.routing';
23 | import { RouterModule } from '@angular/router';
24 |
25 | import { MatInputModule } from '@angular/material/input';
26 | import { MatSidenavModule } from '@angular/material/sidenav';
27 | import { MatToolbarModule } from '@angular/material/toolbar';
28 | import { MatButtonModule } from '@angular/material/button';
29 | import { MatIconModule } from '@angular/material/icon';
30 | import { MatStepperModule } from '@angular/material/stepper';
31 | import { MatSelectModule } from '@angular/material/select';
32 | import { MatTableModule } from '@angular/material/table';
33 | import { MatAutocompleteModule } from '@angular/material/autocomplete';
34 | import { MatExpansionModule } from '@angular/material/expansion';
35 | import { MatSnackBarModule } from '@angular/material/snack-bar';
36 | import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
37 | import { MatSlideToggleModule } from '@angular/material/slide-toggle';
38 | import { MatTooltipModule } from '@angular/material/tooltip';
39 |
40 | import { FormsModule, ReactiveFormsModule } from '@angular/forms';
41 |
42 | import { AppComponent } from './app.component';
43 | import { MainComponent } from './main/main.component';
44 | import { MapComponent } from './map/map.component';
45 | import { TermsComponent } from './terms/terms.component';
46 | import { RuleInputComponent } from './rule/rule.component';
47 | import { FileSizePipe } from './file-size.pipe';
48 | import { environment } from '../environments/environment';
49 |
50 | import {StorageServiceModule} from 'angular-webstorage-service';
51 | import {ColorPickerModule} from 'ngx-color-picker';
52 |
53 | if ( environment.production ) {
54 | enableProdMode();
55 | }
56 |
57 | @NgModule({
58 | declarations: [
59 | AppComponent,
60 | MainComponent,
61 | MapComponent,
62 | TermsComponent,
63 | RuleInputComponent,
64 | FileSizePipe,
65 | ],
66 | imports: [
67 | BrowserModule,
68 | BrowserAnimationsModule,
69 | CodemirrorModule,
70 |
71 | RouterModule.forRoot(routes),
72 |
73 | MatInputModule,
74 | MatSidenavModule,
75 | MatToolbarModule,
76 | MatButtonModule,
77 | MatIconModule,
78 | MatStepperModule,
79 | MatSelectModule,
80 | MatAutocompleteModule,
81 | MatTableModule,
82 | MatExpansionModule,
83 | MatSnackBarModule,
84 | MatProgressSpinnerModule,
85 | MatSlideToggleModule,
86 | MatTooltipModule,
87 | FormsModule,
88 | ReactiveFormsModule,
89 | StorageServiceModule,
90 |
91 | ColorPickerModule
92 | ],
93 | providers: [],
94 | bootstrap: [AppComponent]
95 | })
96 | export class AppModule { }
97 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "rulesDirectory": [
3 | "node_modules/codelyzer"
4 | ],
5 | "rules": {
6 | "arrow-return-shorthand": true,
7 | "callable-types": true,
8 | "class-name": true,
9 | "comment-format": [
10 | true,
11 | "check-space"
12 | ],
13 | "curly": true,
14 | "eofline": true,
15 | "forin": true,
16 | "import-blacklist": [
17 | true,
18 | "rxjs/Rx"
19 | ],
20 | "import-spacing": true,
21 | "indent": [
22 | true,
23 | "spaces"
24 | ],
25 | "interface-over-type-literal": true,
26 | "label-position": true,
27 | "max-line-length": [
28 | true,
29 | 140
30 | ],
31 | "member-access": false,
32 | "member-ordering": [
33 | true,
34 | {
35 | "order": [
36 | "static-field",
37 | "instance-field",
38 | "static-method",
39 | "instance-method"
40 | ]
41 | }
42 | ],
43 | "no-arg": true,
44 | "no-bitwise": true,
45 | "no-console": [
46 | true,
47 | "debug",
48 | "info",
49 | "time",
50 | "timeEnd",
51 | "trace"
52 | ],
53 | "no-construct": true,
54 | "no-debugger": true,
55 | "no-duplicate-super": true,
56 | "no-empty": false,
57 | "no-empty-interface": true,
58 | "no-eval": true,
59 | "no-inferrable-types": [
60 | true,
61 | "ignore-params"
62 | ],
63 | "no-misused-new": true,
64 | "no-non-null-assertion": true,
65 | "no-shadowed-variable": true,
66 | "no-string-literal": false,
67 | "no-string-throw": true,
68 | "no-switch-case-fall-through": true,
69 | "no-trailing-whitespace": true,
70 | "no-unnecessary-initializer": true,
71 | "no-unused-expression": true,
72 | "no-use-before-declare": true,
73 | "no-var-keyword": true,
74 | "object-literal-sort-keys": false,
75 | "one-line": [
76 | true,
77 | "check-open-brace",
78 | "check-catch",
79 | "check-else",
80 | "check-whitespace"
81 | ],
82 | "prefer-const": true,
83 | "quotemark": [
84 | true,
85 | "single"
86 | ],
87 | "radix": true,
88 | "semicolon": [
89 | true,
90 | "always"
91 | ],
92 | "triple-equals": [
93 | true,
94 | "allow-null-check"
95 | ],
96 | "typedef-whitespace": [
97 | true,
98 | {
99 | "call-signature": "nospace",
100 | "index-signature": "nospace",
101 | "parameter": "nospace",
102 | "property-declaration": "nospace",
103 | "variable-declaration": "nospace"
104 | }
105 | ],
106 | "typeof-compare": true,
107 | "unified-signatures": true,
108 | "variable-name": false,
109 | "whitespace": [
110 | true,
111 | "check-branch",
112 | "check-decl",
113 | "check-operator",
114 | "check-separator",
115 | "check-type"
116 | ],
117 | "directive-selector": [
118 | true,
119 | "attribute",
120 | "app",
121 | "camelCase"
122 | ],
123 | "component-selector": [
124 | true,
125 | "element",
126 | "app",
127 | "kebab-case"
128 | ],
129 | "use-input-property-decorator": true,
130 | "use-output-property-decorator": true,
131 | "use-host-property-decorator": true,
132 | "no-input-rename": true,
133 | "no-output-rename": true,
134 | "use-life-cycle-interface": true,
135 | "use-pipe-transform-interface": true,
136 | "component-class-suffix": true,
137 | "directive-class-suffix": true,
138 | "invoke-injectable": true
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "projects": {
6 | "fusiontables2earth": {
7 | "root": "",
8 | "sourceRoot": "src",
9 | "projectType": "application",
10 | "architect": {
11 | "build": {
12 | "builder": "@angular-devkit/build-angular:browser",
13 | "options": {
14 | "outputPath": "dist",
15 | "index": "src/index.html",
16 | "main": "src/main.ts",
17 | "tsConfig": "src/tsconfig.app.json",
18 | "polyfills": "src/polyfills.ts",
19 | "assets": [
20 | "src/assets",
21 | "src/favicon.ico"
22 | ],
23 | "styles": [
24 | "src/theme.scss",
25 | "src/styles.css"
26 | ],
27 | "scripts": []
28 | },
29 | "configurations": {
30 | "production": {
31 | "optimization": true,
32 | "outputHashing": "all",
33 | "sourceMap": false,
34 | "extractCss": true,
35 | "namedChunks": false,
36 | "aot": true,
37 | "extractLicenses": true,
38 | "vendorChunk": false,
39 | "buildOptimizer": true,
40 | "fileReplacements": [
41 | {
42 | "replace": "src/environments/environment.ts",
43 | "with": "src/environments/environment.prod.ts"
44 | }
45 | ]
46 | }
47 | }
48 | },
49 | "serve": {
50 | "builder": "@angular-devkit/build-angular:dev-server",
51 | "options": {
52 | "browserTarget": "fusiontables2earth:build"
53 | },
54 | "configurations": {
55 | "production": {
56 | "browserTarget": "fusiontables2earth:build:production"
57 | }
58 | }
59 | },
60 | "extract-i18n": {
61 | "builder": "@angular-devkit/build-angular:extract-i18n",
62 | "options": {
63 | "browserTarget": "fusiontables2earth:build"
64 | }
65 | },
66 | "test": {
67 | "builder": "@angular-devkit/build-angular:karma",
68 | "options": {
69 | "main": "src/test.ts",
70 | "karmaConfig": "./karma.conf.js",
71 | "polyfills": "src/polyfills.ts",
72 | "tsConfig": "src/tsconfig.spec.json",
73 | "scripts": [],
74 | "styles": [
75 | "src/theme.scss",
76 | "src/styles.css"
77 | ],
78 | "assets": [
79 | "src/assets",
80 | "src/favicon.ico"
81 | ]
82 | }
83 | },
84 | "lint": {
85 | "builder": "@angular-devkit/build-angular:tslint",
86 | "options": {
87 | "tsConfig": [
88 | "src/tsconfig.app.json",
89 | "src/tsconfig.spec.json"
90 | ],
91 | "exclude": [
92 | "**/node_modules/**",
93 | "**/third_party/**"
94 | ]
95 | }
96 | }
97 | }
98 | },
99 | "fusiontables2earth-e2e": {
100 | "root": "e2e",
101 | "sourceRoot": "e2e",
102 | "projectType": "application"
103 | }
104 | },
105 | "defaultProject": "fusiontables2earth",
106 | "schematics": {
107 | "@schematics/angular:component": {
108 | "prefix": "app",
109 | "styleext": "css"
110 | },
111 | "@schematics/angular:directive": {
112 | "prefix": "app"
113 | }
114 | }
115 | }
--------------------------------------------------------------------------------
/src/app/rule/rule.component.html:
--------------------------------------------------------------------------------
1 |
2 | {{ prop.description }}
3 |
4 | Data-driven
5 |
6 |
7 |
12 |
13 |
14 |
18 | None
19 |
24 | {{ fn.name }}
25 |
26 |
27 |
28 |
29 |
33 | None
34 |
35 | {{ column }}
36 |
37 |
38 |
39 |
40 |
Domain
41 |
46 | add_circle
47 |
48 |
52 | remove_circle
53 |
54 |
55 |
57 |
58 |
60 | {{ first ? ('min: ' + stats.min) : ('max: ' + stats.max) }}
61 |
62 |
63 |
64 |
65 |
66 |
Range
67 |
72 | refresh
73 |
74 |
75 |
76 |
79 |
86 | {{d.value}}
87 |
88 |
89 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/src/assets/basemap.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "featureType": "all",
4 | "elementType": "geometry.fill",
5 | "stylers": [
6 | {
7 | "weight": "2.00"
8 | }
9 | ]
10 | },
11 | {
12 | "featureType": "all",
13 | "elementType": "geometry.stroke",
14 | "stylers": [
15 | {
16 | "color": "#9c9c9c"
17 | }
18 | ]
19 | },
20 | {
21 | "featureType": "all",
22 | "elementType": "labels.text",
23 | "stylers": [
24 | {
25 | "visibility": "on"
26 | }
27 | ]
28 | },
29 | {
30 | "featureType": "landscape",
31 | "elementType": "all",
32 | "stylers": [
33 | {
34 | "color": "#f2f2f2"
35 | }
36 | ]
37 | },
38 | {
39 | "featureType": "landscape",
40 | "elementType": "geometry.fill",
41 | "stylers": [
42 | {
43 | "color": "#ffffff"
44 | }
45 | ]
46 | },
47 | {
48 | "featureType": "landscape.man_made",
49 | "elementType": "geometry.fill",
50 | "stylers": [
51 | {
52 | "color": "#ffffff"
53 | }
54 | ]
55 | },
56 | {
57 | "featureType": "poi",
58 | "elementType": "all",
59 | "stylers": [
60 | {
61 | "visibility": "off"
62 | }
63 | ]
64 | },
65 | {
66 | "featureType": "road",
67 | "elementType": "all",
68 | "stylers": [
69 | {
70 | "saturation": -100
71 | },
72 | {
73 | "lightness": 45
74 | }
75 | ]
76 | },
77 | {
78 | "featureType": "road",
79 | "elementType": "geometry.fill",
80 | "stylers": [
81 | {
82 | "color": "#eeeeee"
83 | }
84 | ]
85 | },
86 | {
87 | "featureType": "road",
88 | "elementType": "labels.text.fill",
89 | "stylers": [
90 | {
91 | "color": "#7b7b7b"
92 | }
93 | ]
94 | },
95 | {
96 | "featureType": "road",
97 | "elementType": "labels.text.stroke",
98 | "stylers": [
99 | {
100 | "color": "#ffffff"
101 | }
102 | ]
103 | },
104 | {
105 | "featureType": "road.highway",
106 | "elementType": "all",
107 | "stylers": [
108 | {
109 | "visibility": "simplified"
110 | }
111 | ]
112 | },
113 | {
114 | "featureType": "road.arterial",
115 | "elementType": "labels.icon",
116 | "stylers": [
117 | {
118 | "visibility": "off"
119 | }
120 | ]
121 | },
122 | {
123 | "featureType": "transit",
124 | "elementType": "all",
125 | "stylers": [
126 | {
127 | "visibility": "off"
128 | }
129 | ]
130 | },
131 | {
132 | "featureType": "water",
133 | "elementType": "all",
134 | "stylers": [
135 | {
136 | "color": "#d3e9ef"
137 | },
138 | {
139 | "visibility": "on"
140 | }
141 | ]
142 | },
143 | {
144 | "featureType": "water",
145 | "elementType": "geometry.fill",
146 | "stylers": [
147 | {
148 | "color": "#d3e9ef"
149 | }
150 | ]
151 | },
152 | {
153 | "featureType": "water",
154 | "elementType": "labels.text.fill",
155 | "stylers": [
156 | {
157 | "color": "#070707"
158 | }
159 | ]
160 | },
161 | {
162 | "featureType": "water",
163 | "elementType": "labels.text.stroke",
164 | "stylers": [
165 | {
166 | "color": "#ffffff"
167 | }
168 | ]
169 | }
170 | ]
--------------------------------------------------------------------------------
/src/app/rule/rule.component.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
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 | * https://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 { Component, forwardRef, Input, OnInit } from '@angular/core';
18 | import {
19 | AbstractControl,
20 | FormArray,
21 | FormControl,
22 | FormGroup,
23 | NG_VALUE_ACCESSOR,
24 | NG_VALIDATORS
25 | } from '@angular/forms';
26 | import { StyleFunctions, StyleProp, StyleRule } from '../services/styles.service';
27 | import { ColumnStat } from '../services/bigquery.service';
28 | import { PALETTES } from '../app.constants';
29 |
30 | /**
31 | * Custom form control for a single style rule.
32 | */
33 | @Component({
34 | selector: 'app-rule-input',
35 | templateUrl: './rule.component.html',
36 | styleUrls: ['./rule.component.css'],
37 | providers: [
38 | {
39 | provide: NG_VALUE_ACCESSOR,
40 | useExisting: forwardRef(() => RuleInputComponent),
41 | multi: true
42 | },
43 | {
44 | provide: NG_VALIDATORS,
45 | useExisting: forwardRef(() => RuleInputComponent),
46 | multi: true,
47 | }
48 | ]
49 | })
50 | export class RuleInputComponent implements OnInit {
51 | StyleFunctions = StyleFunctions;
52 |
53 | @Input() formGroup: FormGroup;
54 | @Input() prop: StyleProp;
55 | @Input() stats: ColumnStat;
56 | @Input() columns: Array = [];
57 |
58 | private _rule: StyleRule = {
59 | isComputed: false,
60 | value: '',
61 | function: '',
62 | property: '',
63 | domain: [],
64 | range: []
65 | };
66 |
67 | onChange = (rule: StyleRule) => {};
68 | onTouched = (rule: StyleRule) => {};
69 |
70 | public validate(c: FormControl) {
71 | return null;
72 | }
73 |
74 | ngOnInit() {
75 | // Reflect FormGroup changes to local object used to update view.
76 | this.formGroup.valueChanges.subscribe(() => {
77 | Object.assign(this._rule, this.formGroup.getRawValue());
78 | });
79 | }
80 |
81 | writeValue(rule: StyleRule): void {
82 | Object.assign(this._rule, rule);
83 | this.formGroup.patchValue(rule);
84 | this.onChange(rule);
85 | }
86 |
87 | registerOnChange(fn: (rule: StyleRule) => void): void {
88 | this.onChange = fn;
89 | }
90 |
91 | registerOnTouched(fn: () => void): void {
92 | this.onTouched = fn;
93 | }
94 |
95 | getDomainControls(): AbstractControl[] {
96 | const array = this.formGroup.controls.domain;
97 | return array.controls;
98 | }
99 |
100 | getRangeControls(): AbstractControl[] {
101 | const array = this.formGroup.controls.range;
102 | return array.controls;
103 | }
104 |
105 | addDomainRangeValue() {
106 | const control = new FormControl('');
107 | const domainArray = this.formGroup.controls.domain;
108 | const rangeArray = this.formGroup.controls.range;
109 | domainArray.push(new FormControl(''));
110 | rangeArray.push(new FormControl(''));
111 | }
112 |
113 | removeDomainRangeValue(): void {
114 | const domain = this.formGroup.controls.domain;
115 | const range = this.formGroup.controls.range;
116 | domain.removeAt(domain.length - 1);
117 | range.removeAt(range.length - 1);
118 | }
119 |
120 | /**
121 | * Whether this rule has enough information to be used.
122 | */
123 | isPropEnabled(): boolean {
124 | const rule = this._rule;
125 | if (!rule.isComputed && rule.value) { return true; }
126 | if (rule.isComputed && rule.function) { return true; }
127 | return false;
128 | }
129 |
130 | /**
131 | * Whether this rule requires domain/range mappings.
132 | */
133 | getPropNeedsMapping(): boolean {
134 | return this._rule.function && this._rule.function !== 'identity';
135 | }
136 |
137 | /**
138 | * Replaces current color palette with a random one.
139 | */
140 | addRandomColors() {
141 | const palette = PALETTES[Math.floor(Math.random() * PALETTES.length)];
142 | const range = this.formGroup.controls.range;
143 | if (range.length < 3) {
144 | range.setValue(palette[3].slice(0, range.length));
145 | } else if (range.length < 10) {
146 | range.setValue(palette[range.length]);
147 | } else {
148 | console.warn('No palettes available for 10+ colors.');
149 | }
150 | }
151 |
152 | /**
153 | * ngx-color-picker doesn't support Reactive Forms, so use a change
154 | * handler to update the form.
155 | */
156 | onColorChange() {
157 | this.writeValue(this.formGroup.getRawValue());
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/src/app/services/styles.service.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
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 | * https://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 * as d3Scale from 'd3-scale';
18 | import * as d3Color from 'd3-color';
19 |
20 | export interface StyleRule {
21 | isComputed: boolean;
22 | value: string;
23 | property: string;
24 | function: string;
25 | domain: string[];
26 | range: string[];
27 | }
28 |
29 | const DEFAULT_STYLES = {
30 | fillColor: [255, 0, 0],
31 | fillOpacity: 1.0,
32 | strokeColor: [255, 0, 0],
33 | strokeOpacity: 1.0,
34 | strokeWeight: 1.0,
35 | circleRadius: 25
36 | };
37 |
38 | const parseNumber = Number;
39 | const parseBoolean = (v) => !!String(v).match(/y|1|t/gi);
40 | const parseColorString = (v) => {
41 | const color = d3Color.color(v);
42 | return color ? String(color) : DEFAULT_STYLES.fillColor;
43 | };
44 |
45 | export interface StyleProp {
46 | name: string;
47 | type: string;
48 | description: string;
49 | parse: (i: string) => any;
50 | }
51 |
52 | export const StyleProps: Array = [
53 | {
54 | name: 'fillColor',
55 | type: 'color',
56 | parse: parseColorString,
57 | description: ''
58 | + 'Fill color of a polygon or point. For example, "linear" or "interval" functions may be used'
59 | + ' to map numeric values to a color gradient.'
60 | },
61 | {
62 | name: 'fillOpacity',
63 | type: 'number',
64 | parse: parseNumber,
65 | description: ''
66 | + 'Fill opacity of a polygon or point. Values must be in the range 0—1, where 0=transparent'
67 | + ' and 1=opaque.'
68 | },
69 | {
70 | name: 'strokeColor',
71 | type: 'color',
72 | parse: parseColorString,
73 | description: 'Stroke/outline color of a polygon or line.'},
74 | {
75 | name: 'strokeOpacity',
76 | type: 'number',
77 | parse: parseNumber,
78 | description: ''
79 | + 'Stroke/outline opacity of polygon or line. Values must be in the range 0—1, where'
80 | + ' 0=transparent and 1=opaque.'
81 | },
82 | {
83 | name: 'strokeWeight',
84 | type: 'number',
85 | parse: parseNumber,
86 | description: 'Stroke/outline width, in pixels, of a polygon or line.'},
87 | {
88 | name: 'circleRadius',
89 | type: 'number',
90 | parse: parseNumber,
91 | description: ''
92 | + 'Radius of the circle representing a point, in meters. For example, a "linear" function'
93 | + ' could be used to map numeric values to point sizes, creating a scatterplot style.'
94 | }
95 | ];
96 |
97 | export const StyleFunctions = [
98 | {
99 | name: 'identity',
100 | description: 'Data value of each field is used, verbatim, as the styling value.'},
101 | {
102 | name: 'categorical',
103 | description: 'Data values of each field listed in the domain are mapped 1:1 with corresponding styles in the range.'},
104 | {
105 | name: 'interval',
106 | description: ''
107 | + 'Data values of each field are rounded down to the nearest value in the domain, then styled'
108 | + ' with the corresponding style in the range.'
109 | },
110 | {
111 | name: 'linear',
112 | description: ''
113 | + 'Data values of each field are interpolated linearly across values in the domain, then'
114 | + ' styled with a blend of the corresponding styles in the range.'
115 | },
116 | {
117 | name: 'exponential',
118 | disabled: true,
119 | description: ''
120 | + 'Data values of each field are interpolated exponentially across values in the domain,'
121 | + ' then styled with a blend of the corresponding styles in the range.'
122 | },
123 | ];
124 |
125 | export class StylesService {
126 | iconCache: Map = new Map();
127 | imageCache: Map = new Map();
128 | scaleCache: Map | d3Scale.ScaleLinear | d3Scale.ScaleThreshold>
129 | = new Map();
130 |
131 | constructor () {
132 |
133 | }
134 |
135 | uncache () {
136 | this.scaleCache.clear();
137 | }
138 |
139 | parseStyle (propName: string, row: object, rule: StyleRule) {
140 | const prop = StyleProps.find((p) => p.name === propName);
141 | let scale = this.scaleCache.get(rule);
142 |
143 | if (!rule.isComputed) {
144 | // Static value.
145 | return rule.value
146 | ? prop.parse(rule.value)
147 | : DEFAULT_STYLES[propName];
148 |
149 | } else if (!rule.property || !rule.function) {
150 | // Default value.
151 | return DEFAULT_STYLES[propName];
152 |
153 | } else if (rule.function === 'identity') {
154 | // Identity function.
155 | return prop.parse(row[rule.property]);
156 |
157 | } else if (rule.function === 'categorical') {
158 | // Categorical function.
159 | if (!scale) {
160 | const range = rule.range.map((v) => prop.parse(v));
161 | scale = d3Scale.scaleOrdinal()
162 | .domain(rule.domain)
163 | .range(range)
164 | .unknown(DEFAULT_STYLES[propName]);
165 | this.scaleCache.set(rule, scale);
166 | }
167 | const callableScale = scale as (any) => any;
168 | return callableScale(row[rule.property]);
169 |
170 | } else if (rule.function === 'interval') {
171 | // Interval function.
172 | if (!scale) {
173 | const range = rule.range.map((v) => prop.parse(v));
174 | const tmpScale = d3Scale.scaleThreshold()
175 | .domain(rule.domain.map(Number))
176 | .range([...range, DEFAULT_STYLES[propName]]);
177 | scale = tmpScale as any as d3Scale.ScaleThreshold;
178 | this.scaleCache.set(rule, scale);
179 | }
180 | const callableScale = scale as (number) => any;
181 | return callableScale(Number(row[rule.property]));
182 |
183 | } else if (rule.function === 'linear') {
184 | // Linear function.
185 | if (!scale) {
186 | const range = rule.range.map((v) => prop.parse(v));
187 | scale = d3Scale.scaleLinear()
188 | .domain(rule.domain.map(Number))
189 | .range(range);
190 | this.scaleCache.set(rule, scale);
191 | }
192 | const callableScale = scale as (number) => any;
193 | return callableScale(Number(row[rule.property]));
194 |
195 | }
196 | throw new Error('Unknown style rule function: ' + rule.function);
197 | }
198 |
199 | getIcon (radius: number, color: string, opacity: number) {
200 | const iconCacheKey = `${radius}:${color}:${opacity}`;
201 | const imageCacheKey = `${color}:${opacity}`;
202 |
203 | // Use cached icon if available.
204 | if (this.iconCache.has(iconCacheKey)) { return this.iconCache.get(iconCacheKey); }
205 |
206 | // Use large, scaled icon rather than new image for each size.
207 | const iconRadius = 256;
208 | const iconWidth = 512;
209 |
210 | // Used cached image if available.
211 | if (!this.imageCache.has(imageCacheKey)) {
212 | // Parse color and apply opacity.
213 | const parsedColor = d3Color.color(color);
214 | parsedColor.opacity = opacity;
215 |
216 | // Create canvas and render circle.
217 | const canvas = document.createElement('canvas');
218 | canvas.height = canvas.width = iconWidth;
219 | const ctx = canvas.getContext('2d');
220 | ctx.beginPath();
221 | ctx.arc(iconRadius, iconRadius, iconRadius - 0.5, 0, Math.PI * 2);
222 | ctx.fillStyle = String(parsedColor);
223 | ctx.strokeStyle = null;
224 | ctx.fill();
225 |
226 | // Cache the image.
227 | this.imageCache.set(imageCacheKey, canvas.toDataURL());
228 | }
229 |
230 | // Cache and return result.
231 | const icon = {
232 | url: this.imageCache.get(imageCacheKey),
233 | size: new google.maps.Size(iconWidth, iconWidth),
234 | scaledSize: new google.maps.Size(radius * 2, radius * 2),
235 | anchor: new google.maps.Point(radius, radius)
236 | };
237 | this.iconCache.set(iconCacheKey, icon);
238 | return icon;
239 | }
240 | }
241 |
--------------------------------------------------------------------------------
/src/app/map/map.component.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
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 | * https://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 { Component, ElementRef, Input, NgZone, ViewChild, AfterViewInit, IterableDiffers, IterableDiffer } from '@angular/core';
18 | import { StylesService, StyleRule } from '../services/styles.service';
19 | import { GeoJsonLayer } from '@deck.gl/layers';
20 | import { GoogleMapsOverlay } from '@deck.gl/google-maps';
21 | import bbox from '@turf/bbox';
22 | import { GeoJSONService, GeoJSONFeature } from '../services/geojson.service';
23 |
24 | const LAYER_ID = 'geojson-layer';
25 |
26 | const INITIAL_VIEW_STATE = { latitude: 45, longitude: 0, zoom: 2, pitch: 0 };
27 |
28 | const DEFAULT_BATCH_SIZE = 5;
29 |
30 | @Component({
31 | selector: 'app-map',
32 | templateUrl: './map.component.html',
33 | styleUrls: ['./map.component.css']
34 | })
35 | export class MapComponent implements AfterViewInit {
36 | // DOM element for map.
37 | @ViewChild('mapEl') mapEl: ElementRef;
38 |
39 | // Maps API instance.
40 | map: google.maps.Map;
41 |
42 | // Info window for display over Maps API.
43 | infoWindow: google.maps.InfoWindow;
44 |
45 | // Basemap styles.
46 | pendingStyles: Promise;
47 |
48 | // Styling service.
49 | readonly styler = new StylesService();
50 |
51 | private _rows: object[] = [];
52 | private _features: GeoJSONFeature[] = [];
53 | private _styles: StyleRule[] = [];
54 | private _geoColumn: string;
55 | private _activeGeometryTypes = new Set();
56 |
57 | // Detects how many times we have received new values.
58 | private _numChanges = 0;
59 | // Counts after how many changes we should update the map.
60 | private _batchSize = DEFAULT_BATCH_SIZE;
61 |
62 | private _deckLayer: GoogleMapsOverlay = null;
63 | private _iterableDiffer = null;
64 |
65 | @Input()
66 | set rows(rows: object[]) {
67 | this._rows = rows;
68 | this.resetBatching();
69 | this.updateFeatures();
70 | this.updateStyles();
71 | }
72 |
73 | @Input()
74 | set geoColumn(geoColumn: string) {
75 | this._geoColumn = geoColumn;
76 | this.updateFeatures();
77 | this.updateStyles();
78 | }
79 |
80 | @Input()
81 | set styles(styles: StyleRule[]) {
82 | this._styles = styles;
83 | this.updateStyles();
84 | }
85 |
86 | constructor(private _ngZone: NgZone, iterableDiffers: IterableDiffers) {
87 | this._iterableDiffer = iterableDiffers.find([]).create(null);
88 | this.pendingStyles = fetch('assets/basemap.json', { credentials: 'include' })
89 | .then((response) => response.json());
90 | }
91 |
92 | ngDoCheck() {
93 | let changes = this._iterableDiffer.diff(this._rows);
94 | if (changes) {
95 | this._numChanges++;
96 | if (this._numChanges >= this._batchSize) {
97 | this.updateFeatures();
98 | this.updateStyles();
99 | this._numChanges = 0;
100 | // Increase the batch size incrementally to keep the overhead low.
101 | this._batchSize = this._batchSize * 1.5;
102 | }
103 | }
104 | }
105 |
106 | /**
107 | * Constructs a Maps API instance after DOM has initialized.
108 | */
109 | ngAfterViewInit() {
110 | Promise.all([pendingMap, this.pendingStyles])
111 | .then(([_, mapStyles]) => {
112 | // Initialize Maps API outside of the Angular zone. Maps API binds event listeners,
113 | // and we do NOT want Angular to trigger change detection on these events. Ensuring
114 | // that Maps API interaction doesn't trigger change detection improves performance.
115 | // See: https://blog.angularindepth.com/boosting-performance-of-angular-applications-with-manual-change-detection-42cb396110fb
116 | this._ngZone.runOutsideAngular(() => {
117 | this.map = new google.maps.Map(this.mapEl.nativeElement, {
118 | center: { lat: INITIAL_VIEW_STATE.latitude, lng: INITIAL_VIEW_STATE.longitude },
119 | zoom: INITIAL_VIEW_STATE.zoom,
120 | tilt: 0
121 | });
122 | this.map.setOptions({ styles: mapStyles });
123 | this.infoWindow = new google.maps.InfoWindow({ content: '' });
124 | this.map.data.addListener('click', (e) => {
125 | this.showInfoWindow(e.feature, e.latLng);
126 | });
127 | this._deckLayer = new GoogleMapsOverlay({ layers: [] });
128 | this._deckLayer.setMap(this.map);
129 | this.map.addListener('click', (e) => this._onClick(e));
130 | });
131 | });
132 | }
133 |
134 | _onClick(e: google.maps.MouseEvent) {
135 | // TODO(donmccurdy): Do we need a public API for determining when layer is ready?
136 | if (!this._deckLayer._deck.layerManager) return;
137 |
138 | const { x, y } = e['pixel'];
139 | const picked = this._deckLayer.pickObject({ x, y, radius: 4 });
140 |
141 | if (picked) {
142 | this.showInfoWindow(picked.object, e.latLng);
143 | }
144 | }
145 |
146 | private resetBatching() {
147 | this._numChanges = 0;
148 | this._batchSize = DEFAULT_BATCH_SIZE;
149 | }
150 |
151 | /**
152 | * Converts row objects into GeoJSON, then loads into Maps API.
153 | */
154 | updateFeatures() {
155 | if (!this.map) return;
156 |
157 | this._features = GeoJSONService.rowsToGeoJSON(this._rows, this._geoColumn);
158 |
159 | // Note which types of geometry are being shown.
160 | this._activeGeometryTypes.clear();
161 | this._features.forEach((feature) => {
162 | this._activeGeometryTypes.add(feature.geometry['type']);
163 | });
164 |
165 | // Fit viewport bounds to the data.
166 | const [minX, minY, maxX, maxY] = bbox({ type: 'FeatureCollection', features: this._features });
167 | const bounds = new google.maps.LatLngBounds(
168 | new google.maps.LatLng(minY, minX),
169 | new google.maps.LatLng(maxY, maxX)
170 | );
171 | if (!bounds.isEmpty()) { this.map.fitBounds(bounds); }
172 | }
173 |
174 | /**
175 | * Updates styles applied to all GeoJSON features.
176 | */
177 | updateStyles() {
178 | if (!this.map) return;
179 | this.styler.uncache();
180 |
181 | // Remove old features.
182 | this._deckLayer.setProps({ layers: [] });
183 |
184 | // Create GeoJSON layer.
185 | const colorRe = /(\d+), (\d+), (\d+)/;
186 | const layer = new GeoJsonLayer({
187 | id: LAYER_ID,
188 | data: this._features,
189 | pickable: true,
190 | autoHighlight: true,
191 | highlightColor: [219, 68, 55], // #DB4437
192 | stroked: this.hasStroke(),
193 | filled: true,
194 | extruded: false,
195 | elevationScale: 0,
196 | lineWidthUnits: 'pixels',
197 | pointRadiusMinPixels: 1,
198 | getFillColor: (d) => {
199 | let color = this.getStyle(d, this._styles, 'fillColor');
200 | if (typeof color === 'string') color = color.match(colorRe).slice(1, 4).map(Number);
201 | const opacity = this.getStyle(d, this._styles, 'fillOpacity');
202 | return [...color, opacity * 256];
203 | },
204 | getLineColor: (d) => {
205 | let color = this.getStyle(d, this._styles, 'strokeColor');
206 | if (typeof color === 'string') color = color.match(colorRe).slice(1, 4).map(Number);
207 | const opacity = this.getStyle(d, this._styles, 'strokeOpacity');
208 | return [...color, opacity * 256];
209 | },
210 | getLineWidth: (d) => this.getStyle(d, this._styles, 'strokeWeight'),
211 | getRadius: (d) => this.getStyle(d, this._styles, 'circleRadius'),
212 | });
213 |
214 | this._deckLayer.setProps({ layers: [layer] });
215 | }
216 |
217 | /**
218 | * Return a given style for a given feature.
219 | * @param feature
220 | * @param style
221 | */
222 | getStyle(feature, styles: StyleRule[], styleName: string) {
223 | return this.styler.parseStyle(styleName, feature['properties'], styles[styleName]);
224 | }
225 |
226 | /**
227 | * Returns whether the style is currently enabled.
228 | * @param styles
229 | * @param styleName
230 | */
231 | hasStyle(styles: StyleRule[], styleName: string): boolean {
232 | const rule = styles[styleName];
233 | if (!rule) return false;
234 | if (!rule.isComputed) return !!rule.value || rule.value === '0';
235 | return rule.property && rule.function;
236 | }
237 |
238 | hasStroke() {
239 | return this._activeGeometryTypes.has('LineString')
240 | || this._activeGeometryTypes.has('MultiLineString')
241 | || this._activeGeometryTypes.has('Polygon')
242 | || this._activeGeometryTypes.has('MultiPolygon');
243 | }
244 |
245 | /**
246 | * Displays info window for selected feature.
247 | * @param feature
248 | * @param latLng
249 | */
250 | showInfoWindow(feature: GeoJSONFeature, latLng: google.maps.LatLng) {
251 | this.infoWindow.setContent(`${JSON.stringify(feature.properties, null, 2)} `);
252 | this.infoWindow.open(this.map);
253 | this.infoWindow.setPosition(latLng);
254 | }
255 | }
256 |
--------------------------------------------------------------------------------
/third_party/ng2-codemirror/codemirror.component.css:
--------------------------------------------------------------------------------
1 | /**
2 | * MIT License, Copyright (c) 2016 Simon Babay and Google.
3 | *
4 | * Source: https://github.com/chymz/ng2-codemirror
5 | */
6 |
7 | /* BASICS */
8 |
9 | .CodeMirror {
10 | /* Set height, width, borders, and global font properties here */
11 | font-family: monospace;
12 | height: 300px;
13 | color: black;
14 | direction: ltr;
15 | }
16 |
17 | /* PADDING */
18 |
19 | .CodeMirror-lines {
20 | padding: 4px 0; /* Vertical padding around content */
21 | }
22 | .CodeMirror pre {
23 | padding: 0 4px; /* Horizontal padding of content */
24 | }
25 |
26 | .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
27 | background-color: white; /* The little square between H and V scrollbars */
28 | }
29 |
30 | /* GUTTER */
31 |
32 | .CodeMirror-gutters {
33 | border-right: 1px solid #ddd;
34 | background-color: #f7f7f7;
35 | white-space: nowrap;
36 | }
37 | .CodeMirror-linenumbers {}
38 | .CodeMirror-linenumber {
39 | padding: 0 3px 0 5px;
40 | min-width: 20px;
41 | text-align: right;
42 | color: #999;
43 | white-space: nowrap;
44 | }
45 |
46 | .CodeMirror-guttermarker { color: black; }
47 | .CodeMirror-guttermarker-subtle { color: #999; }
48 |
49 | /* CURSOR */
50 |
51 | .CodeMirror-cursor {
52 | border-left: 1px solid black;
53 | border-right: none;
54 | width: 0;
55 | }
56 | /* Shown when moving in bi-directional text */
57 | .CodeMirror div.CodeMirror-secondarycursor {
58 | border-left: 1px solid silver;
59 | }
60 | .cm-fat-cursor .CodeMirror-cursor {
61 | width: auto;
62 | border: 0 !important;
63 | background: #7e7;
64 | }
65 | .cm-fat-cursor div.CodeMirror-cursors {
66 | z-index: 1;
67 | }
68 | .cm-fat-cursor-mark {
69 | background-color: rgba(20, 255, 20, 0.5);
70 | -webkit-animation: blink 1.06s steps(1) infinite;
71 | -moz-animation: blink 1.06s steps(1) infinite;
72 | animation: blink 1.06s steps(1) infinite;
73 | }
74 | .cm-animate-fat-cursor {
75 | width: auto;
76 | border: 0;
77 | -webkit-animation: blink 1.06s steps(1) infinite;
78 | -moz-animation: blink 1.06s steps(1) infinite;
79 | animation: blink 1.06s steps(1) infinite;
80 | background-color: #7e7;
81 | }
82 | @-moz-keyframes blink {
83 | 0% {}
84 | 50% { background-color: transparent; }
85 | 100% {}
86 | }
87 | @-webkit-keyframes blink {
88 | 0% {}
89 | 50% { background-color: transparent; }
90 | 100% {}
91 | }
92 | @keyframes blink {
93 | 0% {}
94 | 50% { background-color: transparent; }
95 | 100% {}
96 | }
97 |
98 | /* Can style cursor different in overwrite (non-insert) mode */
99 | .CodeMirror-overwrite .CodeMirror-cursor {}
100 |
101 | .cm-tab { display: inline-block; text-decoration: inherit; }
102 |
103 | .CodeMirror-rulers {
104 | position: absolute;
105 | left: 0; right: 0; top: -50px; bottom: -20px;
106 | overflow: hidden;
107 | }
108 | .CodeMirror-ruler {
109 | border-left: 1px solid #ccc;
110 | top: 0; bottom: 0;
111 | position: absolute;
112 | }
113 |
114 | /* DEFAULT THEME */
115 |
116 | .cm-s-default .cm-header {color: blue;}
117 | .cm-s-default .cm-quote {color: #090;}
118 | .cm-negative {color: #d44;}
119 | .cm-positive {color: #292;}
120 | .cm-header, .cm-strong {font-weight: bold;}
121 | .cm-em {font-style: italic;}
122 | .cm-link {text-decoration: underline;}
123 | .cm-strikethrough {text-decoration: line-through;}
124 |
125 | .cm-s-default .cm-keyword {color: #708;}
126 | .cm-s-default .cm-atom {color: #219;}
127 | .cm-s-default .cm-number {color: #164;}
128 | .cm-s-default .cm-def {color: #00f;}
129 | .cm-s-default .cm-variable,
130 | .cm-s-default .cm-punctuation,
131 | .cm-s-default .cm-property,
132 | .cm-s-default .cm-operator {}
133 | .cm-s-default .cm-variable-2 {color: #05a;}
134 | .cm-s-default .cm-variable-3, .cm-s-default .cm-type {color: #085;}
135 | .cm-s-default .cm-comment {color: #a50;}
136 | .cm-s-default .cm-string {color: #a11;}
137 | .cm-s-default .cm-string-2 {color: #f50;}
138 | .cm-s-default .cm-meta {color: #555;}
139 | .cm-s-default .cm-qualifier {color: #555;}
140 | .cm-s-default .cm-builtin {color: #30a;}
141 | .cm-s-default .cm-bracket {color: #997;}
142 | .cm-s-default .cm-tag {color: #170;}
143 | .cm-s-default .cm-attribute {color: #00c;}
144 | .cm-s-default .cm-hr {color: #999;}
145 | .cm-s-default .cm-link {color: #00c;}
146 |
147 | .cm-s-default .cm-error {color: #f00;}
148 | .cm-invalidchar {color: #f00;}
149 |
150 | .CodeMirror-composing { border-bottom: 2px solid; }
151 |
152 | /* Default styles for common addons */
153 |
154 | div.CodeMirror span.CodeMirror-matchingbracket {color: #0b0;}
155 | div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;}
156 | .CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }
157 | .CodeMirror-activeline-background {background: #e8f2ff;}
158 |
159 | /* STOP */
160 |
161 | /* The rest of this file contains styles related to the mechanics of
162 | the editor. You probably shouldn't touch them. */
163 |
164 | .CodeMirror {
165 | position: relative;
166 | overflow: hidden;
167 | background: white;
168 | }
169 |
170 | .CodeMirror-scroll {
171 | overflow: scroll !important; /* Things will break if this is overridden */
172 | /* 30px is the magic margin used to hide the element's real scrollbars */
173 | /* See overflow: hidden in .CodeMirror */
174 | margin-bottom: -30px; margin-right: -30px;
175 | padding-bottom: 30px;
176 | height: 100%;
177 | outline: none; /* Prevent dragging from highlighting the element */
178 | position: relative;
179 | }
180 | .CodeMirror-sizer {
181 | position: relative;
182 | border-right: 30px solid transparent;
183 | }
184 |
185 | /* The fake, visible scrollbars. Used to force redraw during scrolling
186 | before actual scrolling happens, thus preventing shaking and
187 | flickering artifacts. */
188 | .CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
189 | position: absolute;
190 | z-index: 6;
191 | display: none;
192 | }
193 | .CodeMirror-vscrollbar {
194 | right: 0; top: 0;
195 | overflow-x: hidden;
196 | overflow-y: scroll;
197 | }
198 | .CodeMirror-hscrollbar {
199 | bottom: 0; left: 0;
200 | overflow-y: hidden;
201 | overflow-x: scroll;
202 | }
203 | .CodeMirror-scrollbar-filler {
204 | right: 0; bottom: 0;
205 | }
206 | .CodeMirror-gutter-filler {
207 | left: 0; bottom: 0;
208 | }
209 |
210 | .CodeMirror-gutters {
211 | position: absolute; left: 0; top: 0;
212 | min-height: 100%;
213 | z-index: 3;
214 | }
215 | .CodeMirror-gutter {
216 | white-space: normal;
217 | height: 100%;
218 | display: inline-block;
219 | vertical-align: top;
220 | margin-bottom: -30px;
221 | }
222 | .CodeMirror-gutter-wrapper {
223 | position: absolute;
224 | z-index: 4;
225 | background: none !important;
226 | border: none !important;
227 | }
228 | .CodeMirror-gutter-background {
229 | position: absolute;
230 | top: 0; bottom: 0;
231 | z-index: 4;
232 | }
233 | .CodeMirror-gutter-elt {
234 | position: absolute;
235 | cursor: default;
236 | z-index: 4;
237 | }
238 | .CodeMirror-gutter-wrapper ::selection { background-color: transparent }
239 | .CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent }
240 |
241 | .CodeMirror-lines {
242 | cursor: text;
243 | min-height: 1px; /* prevents collapsing before first draw */
244 | }
245 | .CodeMirror pre {
246 | /* Reset some styles that the rest of the page might have set */
247 | -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
248 | border-width: 0;
249 | background: transparent;
250 | font-family: inherit;
251 | font-size: inherit;
252 | margin: 0;
253 | white-space: pre;
254 | word-wrap: normal;
255 | line-height: inherit;
256 | color: inherit;
257 | z-index: 2;
258 | position: relative;
259 | overflow: visible;
260 | -webkit-tap-highlight-color: transparent;
261 | -webkit-font-variant-ligatures: contextual;
262 | font-variant-ligatures: contextual;
263 | }
264 | .CodeMirror-wrap pre {
265 | word-wrap: break-word;
266 | white-space: pre-wrap;
267 | word-break: normal;
268 | }
269 |
270 | .CodeMirror-linebackground {
271 | position: absolute;
272 | left: 0; right: 0; top: 0; bottom: 0;
273 | z-index: 0;
274 | }
275 |
276 | .CodeMirror-linewidget {
277 | position: relative;
278 | z-index: 2;
279 | padding: 0.1px; /* Force widget margins to stay inside of the container */
280 | }
281 |
282 | .CodeMirror-widget {}
283 |
284 | .CodeMirror-rtl pre { direction: rtl; }
285 |
286 | .CodeMirror-code {
287 | outline: none;
288 | }
289 |
290 | /* Force content-box sizing for the elements where we expect it */
291 | .CodeMirror-scroll,
292 | .CodeMirror-sizer,
293 | .CodeMirror-gutter,
294 | .CodeMirror-gutters,
295 | .CodeMirror-linenumber {
296 | -moz-box-sizing: content-box;
297 | box-sizing: content-box;
298 | }
299 |
300 | .CodeMirror-measure {
301 | position: absolute;
302 | width: 100%;
303 | height: 0;
304 | overflow: hidden;
305 | visibility: hidden;
306 | }
307 |
308 | .CodeMirror-cursor {
309 | position: absolute;
310 | pointer-events: none;
311 | }
312 | .CodeMirror-measure pre { position: static; }
313 |
314 | div.CodeMirror-cursors {
315 | visibility: hidden;
316 | position: relative;
317 | z-index: 3;
318 | }
319 | div.CodeMirror-dragcursors {
320 | visibility: visible;
321 | }
322 |
323 | .CodeMirror-focused div.CodeMirror-cursors {
324 | visibility: visible;
325 | }
326 |
327 | .CodeMirror-selected { background: #d9d9d9; }
328 | .CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
329 | .CodeMirror-crosshair { cursor: crosshair; }
330 | .CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; }
331 | .CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; }
332 |
333 | .cm-searching {
334 | background-color: #ffa;
335 | background-color: rgba(255, 255, 0, .4);
336 | }
337 |
338 | /* Used to force a border model for a node */
339 | .cm-force-border { padding-right: .1px; }
340 |
341 | @media print {
342 | /* Hide the cursor when printing */
343 | .CodeMirror div.CodeMirror-cursors {
344 | visibility: hidden;
345 | }
346 | }
347 |
348 | /* See issue #2901 */
349 | .cm-tab-wrap-hack:after { content: ''; }
350 |
351 | /* Help users use markselection to safely style text background */
352 | span.CodeMirror-selectedtext { background: none; }
353 |
--------------------------------------------------------------------------------
/src/app/main/main.component.html:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | Authorize
25 |
26 |
74 |
75 |
76 |
77 |
78 | Add styles
79 |
80 |
100 |
101 |
102 |
103 |
122 |
123 |
124 | IMPORTANT:
125 | Creating a sharing link will save information about the query and style settings. Any user with the link
126 | can restore these settings. However, results returned by the query will not be stored, and the the ability
127 | to execute the query and view the results is restricted to users with the necessary permissions on the
128 | selected Google Cloud Platform project. Sharing links remain active for 30 days.
129 |
130 | Create Share Link
132 |
134 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
151 |
152 |
153 |
154 |
155 |
159 |
160 |
161 |
162 |
--------------------------------------------------------------------------------
/src/app/services/bigquery.service.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
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 | * https://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 { environment } from '../../environments/environment';
18 | import { MAX_RESULTS, TIMEOUT_MS } from '../app.constants';
19 |
20 | export const ColumnType = {
21 | STRING: 'string',
22 | NUMBER: 'number',
23 | LAT: 'latitude',
24 | LNG: 'longitude',
25 | WKT: 'wkt',
26 | DATE: 'date',
27 | ID: 'id'
28 | };
29 |
30 | export interface ColumnStat {
31 | min: number;
32 | max: number;
33 | nulls: number;
34 | }
35 |
36 | export interface Project {
37 | id: string;
38 | }
39 |
40 | export interface Query {
41 | sql: string;
42 | bytesProcessed: number;
43 | }
44 |
45 | export interface BigQueryColumn {
46 | name: string;
47 | type: string;
48 | mode: string;
49 | }
50 |
51 | export interface BigQuerySchema {
52 | fields: BigQueryColumn[];
53 | }
54 |
55 | export interface BigQueryDryRunResponse {
56 | ok: boolean;
57 | totalBytesProcessed?: number;
58 | statementType?: string;
59 | schema?: BigQuerySchema;
60 | }
61 |
62 | export interface BigQueryResponse {
63 | error: string | undefined;
64 | columns: Array | undefined;
65 | columnNames: Array | undefined;
66 | rows: Array | undefined;
67 | totalRows: number;
68 | stats: Map | undefined;
69 | pageToken: string | undefined;
70 | jobID: string | undefined;
71 | totalBytesProcessed?: number;
72 | }
73 |
74 | /**
75 | * Utility class for managing interaction with the Cloud BigQuery API.
76 | */
77 | export class BigQueryService {
78 |
79 | public isSignedIn = false;
80 | public projects: Array = [];
81 |
82 | private signinChangeCallback = () => { };
83 |
84 | /**
85 | * Initializes the service. Must be called before any queries are made.
86 | */
87 | init(): Promise {
88 | // Wait for Google APIs to load, then initialize and try to authenticate.
89 | return pendingGapi
90 | .then(() => {
91 | gapi.client.init({
92 | clientId: environment.authClientID,
93 | scope: environment.authScope
94 | })
95 | .then(() => {
96 | gapi['auth2'].getAuthInstance().isSignedIn.listen(((isSignedIn) => {
97 | this.isSignedIn = isSignedIn;
98 | this.signinChangeCallback();
99 | }));
100 | this.isSignedIn = !!gapi['auth2'].getAuthInstance().isSignedIn.get();
101 | this.signinChangeCallback();
102 | });
103 | });
104 | }
105 |
106 | /**
107 | * Returns current user details.
108 | */
109 | getUser(): Object {
110 | return gapi['auth2'].getAuthInstance().currentUser.get();
111 | }
112 |
113 | /**
114 | * Returns current user credentials.
115 | */
116 | getCredential(): Object {
117 | let authResponse = gapi['auth2'].getAuthInstance().currentUser.get().getAuthResponse(true);
118 | if (authResponse) {
119 | return { id_token: authResponse.id_token, access_token: authResponse.access_token };
120 | }
121 | return null;
122 | }
123 |
124 | /**
125 | * Attempts session login.
126 | */
127 | signin() {
128 | gapi['auth2'].getAuthInstance().signIn().then(() => this.signinChangeCallback());
129 | }
130 |
131 | /**
132 | * Logs out of current session.
133 | */
134 | signout() {
135 | this.isSignedIn = false;
136 | gapi['auth2'].getAuthInstance().signOut().then(() => this.signinChangeCallback());
137 | }
138 |
139 | /**
140 | * Sets callback to be invoked when signin status changes.
141 | * @param callback
142 | */
143 | onSigninChange(callback): void {
144 | this.signinChangeCallback = callback;
145 | }
146 |
147 | /**
148 | * Queries and returns a list of GCP projects available to the current user.
149 | */
150 | getProjects(): Promise> {
151 | if (this.projects.length) { return Promise.resolve(this.projects); }
152 |
153 | return gapi.client.request({ path: `https://www.googleapis.com/bigquery/v2/projects?maxResults=100000` })
154 | .then((response) => {
155 | this.projects = response.result.projects.slice();
156 | this.projects.sort((p1, p2) => p1['id'] > p2['id'] ? 1 : -1);
157 | return >this.projects;
158 | });
159 | }
160 |
161 | /**
162 | * Queries and returns the sql text for a specific job ID. Throws an error if the
163 | * job id is not for a SELECT statement.
164 | */
165 | getQueryFromJob(jobID: string, location: string, projectID: string): Promise {
166 | const location_param = location ? `location=${location}` : '';
167 | return gapi.client.request({
168 | path: `https://www.googleapis.com/bigquery/v2/projects/${projectID}/jobs/${jobID}?${location_param}`
169 | }).then((response) => {
170 | if (!response.result.statistics.query) {
171 | throw new Error('Job id is not for a query job.');
172 | }
173 | if (response.result.statistics.query.statementType != 'SELECT') {
174 | throw new Error('Job id is not for a SELECT statement.');
175 | }
176 | return { sql: response.result.configuration.query.query, bytesProcessed: Number(response.result.statistics.query.totalBytesProcessed) };
177 | });
178 | }
179 |
180 | /**
181 | * Performs a dry run for the given query, and returns estimated bytes to be processed.
182 | * If the dry run fails, returns -1.
183 | * @param projectID
184 | * @param sql
185 | */
186 | prequery(projectID: string, sql: string, location: string): Promise {
187 | const configuration = {
188 | dryRun: true,
189 | query: {
190 | query: sql,
191 | maxResults: MAX_RESULTS,
192 | timeoutMs: TIMEOUT_MS,
193 | useLegacySql: false
194 | }
195 | };
196 | if (location) { configuration.query['location'] = location; }
197 | return gapi.client.request({
198 | path: `https://www.googleapis.com/bigquery/v2/projects/${projectID}/jobs`,
199 | method: 'POST',
200 | body: { configuration },
201 | }).then((response) => {
202 | const { schema, statementType } = response.result.statistics.query;
203 | const totalBytesProcessed = Number(response.result.statistics.query.totalBytesProcessed);
204 | return { ok: true, schema, statementType, totalBytesProcessed };
205 | }).catch((e) => {
206 | if (e && e.result && e.result.error) {
207 | throw new Error(e.result.error.message);
208 | }
209 | console.warn(e);
210 | return { ok: false };
211 | });
212 | }
213 |
214 | normalizeRows(rows: Array, normalizedCols: Array, stats: Map) {
215 | return (rows || []).map((row) => {
216 | const rowObject = {};
217 | row['f'].forEach(({ v }, index) => {
218 | const column = normalizedCols[index];
219 | if (column['type'] === ColumnType.NUMBER) {
220 | v = v === '' || v === null ? null : Number(v);
221 | rowObject[column['name']] = v;
222 | const stat = stats.get(column['name']);
223 | if (v === null) {
224 | stat.nulls++;
225 | } else {
226 | stat.max = Math.round(Math.max(stat.max, v) * 1000) / 1000;
227 | stat.min = Math.round(Math.min(stat.min, v) * 1000) / 1000;
228 | }
229 | } else {
230 | rowObject[column['name']] = v === null ? null : String(v);
231 | }
232 | });
233 | return rowObject;
234 | });
235 | }
236 |
237 | getResults(projectID: string, jobID: string, location: string, pageToken: string, normalized_cols: Array, stats: Map): Promise {
238 | const body = {
239 | maxResults: MAX_RESULTS,
240 | timeoutMs: TIMEOUT_MS,
241 | pageToken: pageToken
242 | };
243 | if (location) { body['location'] = location; }
244 |
245 | return gapi.client.request({
246 | path: `https://www.googleapis.com/bigquery/v2/projects/${projectID}/queries/${jobID}`,
247 | method: 'GET',
248 | params: body,
249 | }).then((response) => {
250 | if (response.result.jobComplete === false) {
251 | throw new Error(`Request timed out after ${TIMEOUT_MS / 1000} seconds. This UI does not yet handle longer jobs.`);
252 | }
253 | // Normalize row structure.
254 | const rows = this.normalizeRows(response.result.rows, normalized_cols, stats);
255 |
256 | return { rows, stats, pageToken: response.result.pageToken } as BigQueryResponse;
257 | });
258 | }
259 |
260 | query(projectID: string, sql: string, location: string): Promise {
261 | const body = {
262 | query: sql,
263 | maxResults: MAX_RESULTS,
264 | timeoutMs: TIMEOUT_MS,
265 | useLegacySql: false
266 | };
267 | if (location) { body['location'] = location; }
268 | return gapi.client.request({
269 | path: `https://www.googleapis.com/bigquery/v2/projects/${projectID}/queries`,
270 | method: 'POST',
271 | body,
272 | }).then((response) => {
273 | const stats = new Map();
274 |
275 | if (response.result.jobComplete === false) {
276 | throw new Error(`Request timed out after ${TIMEOUT_MS / 1000} seconds. This UI does not yet handle longer jobs.`);
277 | }
278 |
279 | // Normalize column types.
280 | const columnNames = [];
281 | const columns = (response.result.schema.fields || []).map((field) => {
282 | if (isNumericField(field)) {
283 | field.type = ColumnType.NUMBER;
284 | stats.set(field.name, { min: Infinity, max: -Infinity, nulls: 0 });
285 | } else {
286 | field.type = ColumnType.STRING;
287 | }
288 | columnNames.push(field.name);
289 | return field;
290 | });
291 |
292 | // Normalize row structure.
293 | const rows = this.normalizeRows(response.result.rows, columns, stats);
294 |
295 | if (rows.length === 0) {
296 | throw new Error('No results.');
297 | }
298 |
299 | const totalRows = Number(response.result.totalRows);
300 |
301 | return { columns, columnNames, rows, stats, totalRows, pageToken: response.result.pageToken, jobID: response.result.jobReference.jobId,
302 | totalBytesProcessed: Number(response.result.totalBytesProcessed)} as BigQueryResponse;
303 | });
304 | }
305 | }
306 |
307 | function isNumericField(field: Object) {
308 | const fieldType = field['type'].toUpperCase();
309 | return ['INTEGER', 'NUMBER', 'FLOAT', 'DECIMAL'].includes(fieldType);
310 | }
311 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/src/app/main/main.component.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google LLC
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 | * https://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 { Component, ChangeDetectorRef, Inject, NgZone, OnInit, OnDestroy } from '@angular/core';
18 | import { ActivatedRoute } from '@angular/router';
19 | import { FormBuilder, FormGroup, FormControl, FormArray, Validators } from '@angular/forms';
20 | import { LOCAL_STORAGE, WebStorageService } from 'angular-webstorage-service';
21 | import { MatTableDataSource, MatSnackBar } from '@angular/material';
22 | import { StepperSelectionEvent } from '@angular/cdk/stepper';
23 | import { Subject } from 'rxjs/Subject';
24 | import { Subscription } from 'rxjs/Subscription';
25 | import 'rxjs/add/operator/debounceTime';
26 | import 'rxjs/add/operator/map';
27 | import * as CryptoJS from "crypto-js";
28 |
29 | import { StyleProps, StyleRule } from '../services/styles.service';
30 | import {
31 | BigQueryService,
32 | BigQueryColumn,
33 | ColumnStat,
34 | Project,
35 | BigQueryDryRunResponse,
36 | BigQueryResponse
37 | } from '../services/bigquery.service';
38 |
39 | import { FirestoreService, ShareableData } from '../services/firestore.service'
40 | import {
41 | Step,
42 | SAMPLE_QUERY,
43 | SAMPLE_FILL_COLOR,
44 | SAMPLE_FILL_OPACITY,
45 | MAX_RESULTS_PREVIEW,
46 | SAMPLE_CIRCLE_RADIUS,
47 | SHARING_VERSION,
48 | MAX_RESULTS,
49 | MAX_PAGES
50 |
51 | } from '../app.constants';
52 |
53 | const DEBOUNCE_MS = 1000;
54 | const USER_QUERY_START_MARKER = '--__USER__QUERY__START__';
55 | const USER_QUERY_END_MARKER = '--__USER__QUERY__END__';
56 |
57 | @Component({
58 | selector: 'app-main',
59 | templateUrl: './main.component.html',
60 | styleUrls: ['./main.component.css']
61 | })
62 |
63 | export class MainComponent implements OnInit, OnDestroy {
64 | readonly title = 'BigQuery Geo Viz';
65 | readonly StyleProps = StyleProps;
66 | readonly projectIDRegExp = new RegExp('^[a-z][a-z0-9\.:-]*$', 'i');
67 | readonly datasetIDRegExp = new RegExp('^[_a-z][a-z_0-9]*$', 'i');
68 | readonly tableIDRegExp = new RegExp('^[a-z][a-z_0-9]*$', 'i');
69 | readonly jobIDRegExp = new RegExp('[a-z0-9_-]*$', 'i');
70 | readonly localStorageKey = 'execution_local_storage_key';
71 |
72 | // GCP session data
73 | readonly dataService = new BigQueryService();
74 | readonly storageService = new FirestoreService();
75 | isSignedIn: boolean;
76 | user: Object;
77 | matchingProjects: Array = [];
78 |
79 | // Form groups
80 | dataFormGroup: FormGroup;
81 | schemaFormGroup: FormGroup;
82 | stylesFormGroup: FormGroup;
83 | sharingFormGroup: FormGroup;
84 |
85 | // BigQuery response data
86 | columns: Array;
87 | columnNames: Array;
88 | geoColumnNames: Array;
89 | projectID = '';
90 | dataset = '';
91 | table = '';
92 | jobID = '';
93 | location = '';
94 | // This contains the query that ran in the job.
95 | jobWrappedSql = '';
96 | bytesProcessed: number = 0;
97 | lintMessage = '';
98 | pending = false;
99 | rows: Array;
100 | totalRows: number = 0;
101 | maxRows: number = MAX_RESULTS;
102 | data: MatTableDataSource;
103 | stats: Map = new Map();
104 | sideNavOpened: boolean = true;
105 | // If a new query is run or the styling has changed, we need to generate a new sharing id.
106 | sharingDataChanged = false;
107 | // Track if the stepper has actually changed.
108 | stepperChanged = false;
109 | sharingId = ''; // This is the input sharing Id from the url
110 | generatedSharingId = ''; // This is the sharing id generated for the current settings.
111 | sharingIdGenerationPending = false;
112 |
113 | // UI state
114 | stepIndex: Number = 0;
115 |
116 | // Current style rules
117 | styles: Array = [];
118 |
119 | // CodeMirror configuration
120 | readonly cmConfig = {
121 | indentWithTabs: true,
122 | smartIndent: true,
123 | lineNumbers: true,
124 | lineWrapping: true
125 | };
126 | readonly cmDebouncer: Subject = new Subject();
127 | cmDebouncerSub: Subscription;
128 |
129 | constructor(
130 | @Inject(LOCAL_STORAGE) private _storage: WebStorageService,
131 | private _formBuilder: FormBuilder,
132 | private _snackbar: MatSnackBar,
133 | private _changeDetectorRef: ChangeDetectorRef,
134 | private _route: ActivatedRoute,
135 | private _ngZone: NgZone) {
136 |
137 | // Debounce CodeMirror change events to avoid running extra dry runs.
138 | this.cmDebouncerSub = this.cmDebouncer
139 | .debounceTime(DEBOUNCE_MS)
140 | .subscribe((value: string) => { this._dryRun(); });
141 |
142 | // Set up BigQuery service.
143 | this.dataService.onSigninChange(() => this.onSigninChange());
144 | this.dataService.init()
145 | .catch((e) => this.showMessage(parseErrorMessage(e)));
146 | }
147 |
148 | ngOnInit() {
149 | this.columns = [];
150 | this.columnNames = [];
151 | this.geoColumnNames = [];
152 | this.rows = [];
153 |
154 | // Read parameters from URL
155 | this.projectID = this._route.snapshot.paramMap.get("project");
156 | this.dataset = this._route.snapshot.paramMap.get("dataset");
157 | this.table = this._route.snapshot.paramMap.get("table");
158 | this.jobID = this._route.snapshot.paramMap.get("job");
159 | this.location = this._route.snapshot.paramMap.get("location") || ''; // Empty string for 'Auto Select'
160 | this.sharingId = this._route.snapshot.queryParams["shareid"];
161 |
162 | // Data form group
163 | this.dataFormGroup = this._formBuilder.group({
164 | projectID: ['', Validators.required],
165 | sql: ['', Validators.required],
166 | location: [''],
167 | });
168 | this.dataFormGroup.controls.projectID.valueChanges.debounceTime(200).subscribe(() => {
169 | this.dataService.getProjects()
170 | .then((projects) => {
171 | this.matchingProjects = projects.filter((project) => {
172 | return project['id'].indexOf(this.dataFormGroup.controls.projectID.value) >= 0;
173 | });
174 | });
175 | });
176 |
177 | // Schema form group
178 | this.schemaFormGroup = this._formBuilder.group({ geoColumn: [''] });
179 |
180 | // Style rules form group
181 | const stylesGroupMap = {};
182 | StyleProps.forEach((prop) => stylesGroupMap[prop.name] = this.createStyleFormGroup());
183 | this.stylesFormGroup = this._formBuilder.group(stylesGroupMap);
184 |
185 | // Sharing form group
186 | this.sharingFormGroup = this._formBuilder.group({
187 | sharingUrl: '',
188 | });
189 |
190 | // Initialize default styles.
191 | this.updateStyles();
192 | }
193 |
194 | saveDataToSharedStorage() {
195 | const dataValues = this.dataFormGroup.getRawValue();
196 | // Encrypt the style values using the sql string.
197 | const hashedStyleValues = CryptoJS.AES.encrypt(JSON.stringify(this.styles), this.jobWrappedSql + this.bytesProcessed);
198 | const shareableData = {
199 | sharingVersion: SHARING_VERSION,
200 | projectID: dataValues.projectID,
201 | jobID: this.jobID,
202 | location: dataValues.location,
203 | styles: hashedStyleValues.toString(),
204 | creationTimestampMs: Date.now()
205 | };
206 | return this.storageService.storeShareableData(shareableData).then((written_doc_id) => {
207 | this.generatedSharingId = written_doc_id;
208 | })
209 | }
210 |
211 | restoreDataFromSharedStorage(docId: string): Promise {
212 | return this.storageService.getSharedData(this.sharingId);
213 | }
214 |
215 | saveDataToLocalStorage(projectID: string, sql: string, location: string) {
216 | this._storage.set(this.localStorageKey, { projectID: projectID, sql: sql, location: location });
217 | }
218 |
219 | loadDataFromLocalStorage(): { projectID: string, sql: string, location: string } {
220 | return this._storage.get(this.localStorageKey);
221 | }
222 |
223 | clearDataFromLocalStorage() {
224 | this._storage.remove(this.localStorageKey);
225 | }
226 |
227 | resetUIOnSingout() {
228 | this.clearDataFromLocalStorage();
229 | this.dataFormGroup.reset();
230 | this.lintMessage = '';
231 | }
232 |
233 | ngOnDestroy() {
234 | this.cmDebouncerSub.unsubscribe();
235 | }
236 |
237 | signin() {
238 | this.clearDataFromLocalStorage();
239 | this.dataService.signin();
240 | }
241 |
242 | signout() {
243 | this.resetUIOnSingout();
244 | this.dataService.signout();
245 | }
246 |
247 | onSigninChange() {
248 | this._ngZone.run(() => {
249 | this.isSignedIn = this.dataService.isSignedIn;
250 | if (!this.dataService.isSignedIn) { return; }
251 | this.user = this.dataService.getUser();
252 | this.storageService.authorize(this.dataService.getCredential());
253 | this.dataService.getProjects()
254 | .then((projects) => {
255 | this.matchingProjects = projects;
256 | this._changeDetectorRef.detectChanges();
257 | });
258 |
259 | if (this._hasJobParams() && this._jobParamsValid()) {
260 | this.dataFormGroup.patchValue({
261 | sql: '/* Loading sql query from job... */',
262 | projectID: this.projectID,
263 | location: this.location
264 | });
265 |
266 | this.dataService.getQueryFromJob(this.jobID, this.location, this.projectID).then((queryText) => {
267 | this.dataFormGroup.patchValue({
268 | sql: queryText.sql,
269 | });
270 | });
271 | } else if (this._hasTableParams() && this._tableParamsValid()) {
272 | this.dataFormGroup.patchValue({
273 | sql: `SELECT * FROM \`${this.projectID}.${this.dataset}.${this.table}\`;`,
274 | projectID: this.projectID,
275 | });
276 | } else if (this.sharingId) {
277 | this.restoreDataFromSharedStorage(this.sharingId).then((shareableValues) => {
278 | this.applyRetrievedSharingValues(shareableValues);
279 | }).catch((e) => this.showMessage(parseErrorMessage(e)));
280 | } else {
281 | const localStorageValues = this.loadDataFromLocalStorage();
282 | if (localStorageValues) {
283 | this.dataFormGroup.patchValue({
284 | sql: localStorageValues.sql,
285 | projectID: localStorageValues.projectID,
286 | location: localStorageValues.location
287 | });
288 | }
289 | }
290 | });
291 | }
292 |
293 | applyRetrievedSharingValues(shareableValues: ShareableData) {
294 | if (shareableValues) {
295 | if (shareableValues.sharingVersion != SHARING_VERSION) {
296 | throw new Error('Sharing link is invalid.');
297 | }
298 | this.dataFormGroup.patchValue({
299 | sql: '/* Loading sql query from job... */',
300 | projectID: shareableValues.projectID,
301 | location: shareableValues.location
302 | });
303 | this.dataService.getQueryFromJob(shareableValues.jobID, shareableValues.location, shareableValues.projectID).then((queryText) => {
304 | this.dataFormGroup.patchValue({
305 | sql: this.convertToUserQuery(queryText.sql),
306 | });
307 | const unencryptedStyles = JSON.parse(CryptoJS.enc.Utf8.stringify(CryptoJS.AES.decrypt(shareableValues.styles, queryText.sql + queryText.bytesProcessed)));
308 | this.setNumStops(this.stylesFormGroup.controls.fillColor, unencryptedStyles['fillColor'].domain.length);
309 | this.setNumStops(this.stylesFormGroup.controls.fillOpacity, unencryptedStyles['fillOpacity'].domain.length);
310 | this.setNumStops(this.stylesFormGroup.controls.strokeColor, unencryptedStyles['strokeColor'].domain.length);
311 | this.setNumStops(this.stylesFormGroup.controls.strokeOpacity, unencryptedStyles['strokeOpacity'].domain.length);
312 | this.setNumStops(this.stylesFormGroup.controls.strokeWeight, unencryptedStyles['strokeWeight'].domain.length);
313 | this.setNumStops(this.stylesFormGroup.controls.circleRadius, unencryptedStyles['circleRadius'].domain.length);
314 | this.stylesFormGroup.patchValue(unencryptedStyles);
315 | this.updateStyles();
316 | }).catch((e) => this.showMessage("Cannot retrieve styling options."));
317 | }
318 | }
319 |
320 | clearGeneratedSharingUrl() {
321 | this.generatedSharingId = '';
322 | this.sharingDataChanged = true;
323 | this.sharingFormGroup.patchValue({
324 | sharingUrl: ''
325 | });
326 | }
327 |
328 | generateSharingUrl() {
329 | if (!this._hasJobParams()) {
330 | this.showMessage("Please first run a valid query before generating a sharing URL.");
331 | return;
332 | }
333 | if (this.stepIndex == Step.SHARE && this.stepperChanged && this.sharingDataChanged) {
334 | this.sharingDataChanged = false;
335 | this.sharingIdGenerationPending = true;
336 | this.saveDataToSharedStorage().then(() => {
337 | this.sharingFormGroup.patchValue({
338 | sharingUrl: window.location.origin + '?shareid=' + this.generatedSharingId
339 | });
340 | }).catch((e) => this.showMessage(parseErrorMessage(e)));
341 | }
342 | this.sharingIdGenerationPending = false;
343 | }
344 |
345 | onStepperChange(e: StepperSelectionEvent) {
346 | this.stepIndex = e.selectedIndex;
347 | if (e.selectedIndex != e.previouslySelectedIndex) {
348 | this.stepperChanged = true;
349 | } else {
350 | this.stepperChanged = false;
351 | }
352 | gtag('event', 'step', { event_label: `step ${this.stepIndex}` });
353 | }
354 |
355 | dryRun() {
356 | this.cmDebouncer.next();
357 | }
358 |
359 | _hasJobParams(): boolean {
360 | return !!(this.jobID && this.projectID);
361 | }
362 |
363 | _hasTableParams(): boolean {
364 | return !!(this.projectID && this.dataset && this.table);
365 | }
366 |
367 | _jobParamsValid(): boolean {
368 | return this.projectIDRegExp.test(this.projectID) &&
369 | this.jobIDRegExp.test(this.jobID);
370 | }
371 | _tableParamsValid(): boolean {
372 | return this.projectIDRegExp.test(this.projectID) &&
373 | this.datasetIDRegExp.test(this.dataset) &&
374 | this.tableIDRegExp.test(this.table);
375 | }
376 |
377 | _dryRun(): Promise {
378 | const { projectID, sql, location } = this.dataFormGroup.getRawValue();
379 | if (!projectID) return;
380 | const dryRun = this.dataService.prequery(projectID, sql, location)
381 | .then((response: BigQueryDryRunResponse) => {
382 | if (!response.ok) throw new Error('Query analysis failed.');
383 | const geoColumn = response.schema.fields.find((f) => f.type === 'GEOGRAPHY');
384 | if (response.statementType !== 'SELECT') {
385 | throw new Error('Expected a SELECT statement.');
386 | } else if (!geoColumn) {
387 | throw new Error('Expected a geography column, but found none.');
388 | }
389 | this.lintMessage = '';
390 | this.bytesProcessed = response.totalBytesProcessed;
391 | return response;
392 | });
393 | dryRun.catch((e) => {
394 | this.bytesProcessed = -1;
395 | this.lintMessage = parseErrorMessage(e);
396 | });
397 | return dryRun;
398 | }
399 |
400 | // 'count' is used to track the number of request. Each request is 10MB.
401 | getResults(count: number, projectID: string, inputPageToken: string, location: string, jobID: string): Promise {
402 | if (!inputPageToken || count >= MAX_PAGES) {
403 | // Force an update feature here since everything is done.
404 | this.rows = this.rows.slice(0);
405 | return;
406 | }
407 | count = count + 1;
408 | return this.dataService.getResults(projectID, jobID, location, inputPageToken, this.columns, this.stats).then(({ rows, stats, pageToken }) => {
409 | this.rows.push(...rows);
410 | this.stats = stats;
411 | this._changeDetectorRef.detectChanges();
412 | return this.getResults(count, projectID, pageToken, location, jobID);
413 | });
414 | }
415 |
416 | convertToUserQuery(geovizQuery: string): string {
417 | if (!geovizQuery) return '';
418 |
419 | const lines = geovizQuery.split('\n');
420 | let userQueryStarted = false;
421 | let userQuery = '';
422 | lines.forEach((line) => {
423 | if (line.includes(USER_QUERY_START_MARKER)) {
424 | userQueryStarted = true;
425 | } else if (line.includes(USER_QUERY_END_MARKER)) {
426 | userQueryStarted = false;
427 | } else {
428 | if (userQueryStarted) {
429 | userQuery += line + '\n';
430 | }
431 | }
432 | });
433 |
434 | return userQuery.trim();
435 | }
436 |
437 | convertToGeovizQuery(userQuery: string, geoColumns: BigQueryColumn[], numCols: number): string {
438 | const hasNonGeoColumns = geoColumns.length < numCols;
439 | const nonGeoClause = hasNonGeoColumns
440 | ? `* EXCEPT(${geoColumns.map((f) => `\`${f.name}\``).join(', ')}),`
441 | : '';
442 | return `SELECT
443 | ${nonGeoClause}
444 | ${ geoColumns.map((f) => `ST_AsGeoJson(\`${f.name}\`) as \`${f.name}\``).join(', ')}
445 | FROM (
446 | ${USER_QUERY_START_MARKER}\n
447 | ${userQuery.replace(/;\s*$/, '')}\n
448 | ${USER_QUERY_END_MARKER}\n
449 | );`;
450 | }
451 |
452 | query() {
453 | if (this.pending) { return; }
454 | this.pending = true;
455 |
456 | // We will save the query information to local store to be restored next
457 | // time that the app is launched.
458 | const dataFormValues = this.dataFormGroup.getRawValue();
459 | this.projectID = dataFormValues.projectID;
460 | const sql = dataFormValues.sql;
461 | this.location = dataFormValues.location;
462 | this.saveDataToLocalStorage(this.projectID, sql, this.location);
463 |
464 | // Clear the existing sharing URL.
465 | this.clearGeneratedSharingUrl();
466 |
467 | let geoColumns;
468 |
469 | this._dryRun()
470 | .then((dryRunResponse) => {
471 | geoColumns = dryRunResponse.schema.fields.filter((f) => f.type === 'GEOGRAPHY');
472 |
473 | // Wrap the user's SQL query, replacing geography columns with GeoJSON.
474 | this.jobWrappedSql = this.convertToGeovizQuery(sql, geoColumns, dryRunResponse.schema.fields.length);
475 | return this.dataService.query(this.projectID, this.jobWrappedSql, this.location);
476 | })
477 | .then(({ columns, columnNames, rows, stats, totalRows, pageToken, jobID, totalBytesProcessed }) => {
478 | this.columns = columns;
479 | this.columnNames = columnNames;
480 | this.geoColumnNames = geoColumns.map((f) => f.name);
481 | this.rows = rows;
482 | this.stats = stats;
483 | this.data = new MatTableDataSource(rows.slice(0, MAX_RESULTS_PREVIEW));
484 | this.schemaFormGroup.patchValue({ geoColumn: geoColumns[0].name });
485 | this.totalRows = totalRows;
486 | this.jobID = jobID;
487 | this.bytesProcessed = totalBytesProcessed;
488 | return this.getResults(0, this.projectID, pageToken, this.location, jobID);
489 | })
490 | .catch((e) => {
491 | const error = e && e.result && e.result.error || {};
492 | if (error.status === 'INVALID_ARGUMENT' && error.message.match(/^Unrecognized name: f\d+_/)) {
493 | this.showMessage(
494 | 'Geography columns must provide a name. For example, "SELECT ST_GEOGPOINT(1,2)" could ' +
495 | 'be changed to "SELECT ST_GEOGPOINT(1,2) geo".'
496 | );
497 | } else {
498 | this.showMessage(parseErrorMessage(e));
499 | }
500 | })
501 | .then(() => {
502 | this.pending = false;
503 | this._changeDetectorRef.detectChanges();
504 | });
505 |
506 | }
507 |
508 | onApplyStylesClicked() {
509 | this.clearGeneratedSharingUrl();
510 | this.updateStyles();
511 | }
512 |
513 | updateStyles() {
514 | if (this.stylesFormGroup.invalid) { return; }
515 | this.styles = this.stylesFormGroup.getRawValue();
516 | }
517 |
518 | getRowWidth() {
519 | return (this.columns.length * 100) + 'px';
520 | }
521 |
522 | onFillPreset() {
523 | switch (this.stepIndex) {
524 | case Step.DATA:
525 | this.dataFormGroup.patchValue({ sql: SAMPLE_QUERY });
526 | break;
527 | case Step.SCHEMA:
528 | this.schemaFormGroup.patchValue({ geoColumn: 'WKT' });
529 | break;
530 | case Step.STYLE:
531 | this.setNumStops(this.stylesFormGroup.controls.fillColor, SAMPLE_FILL_COLOR.domain.length);
532 | this.setNumStops(this.stylesFormGroup.controls.circleRadius, SAMPLE_CIRCLE_RADIUS.domain.length);
533 | this.stylesFormGroup.controls.fillOpacity.patchValue(SAMPLE_FILL_OPACITY);
534 | this.stylesFormGroup.controls.fillColor.patchValue(SAMPLE_FILL_COLOR);
535 | this.stylesFormGroup.controls.circleRadius.patchValue(SAMPLE_CIRCLE_RADIUS);
536 | break;
537 | default:
538 | console.warn(`Unexpected step index, ${this.stepIndex}.`);
539 | }
540 |
541 | gtag('event', 'preset', { event_label: `step ${this.stepIndex}` });
542 | }
543 |
544 | setNumStops(group: FormGroup, numStops: number): void {
545 | const domain = group.controls.domain;
546 | const range = group.controls.range;
547 | while (domain.length !== numStops) {
548 | if (domain.length < numStops) {
549 | domain.push(new FormControl(''));
550 | range.push(new FormControl(''));
551 | }
552 | if (domain.length > numStops) {
553 | domain.removeAt(domain.length - 1);
554 | range.removeAt(range.length - 1);
555 | }
556 | }
557 | }
558 |
559 | createStyleFormGroup(): FormGroup {
560 | return this._formBuilder.group({
561 | isComputed: [false],
562 | value: [''],
563 | property: [''],
564 | function: [''],
565 | domain: this._formBuilder.array([[''], ['']]),
566 | range: this._formBuilder.array([[''], ['']])
567 | });
568 | }
569 |
570 | getPropStatus(propName: string): string {
571 | const rule = this.stylesFormGroup.controls[propName].value;
572 | if (!rule.isComputed && rule.value) { return 'global'; }
573 | if (rule.isComputed && rule.function) { return 'computed'; }
574 | return 'none';
575 | }
576 |
577 | getPropStats(propName: string): ColumnStat {
578 | const group = this.stylesFormGroup.controls[propName];
579 | const rawValue = group.value;
580 | if (!rawValue.property) { return null; }
581 | return this.stats.get(rawValue.property);
582 | }
583 |
584 | getPropFormGroup(propName: string): FormGroup {
585 | return this.stylesFormGroup.controls[propName];
586 | }
587 |
588 | showMessage(message: string, duration: number = 5000) {
589 | console.warn(message);
590 | this._ngZone.run(() => {
591 | this._snackbar.open(message, undefined, { duration: duration });
592 | });
593 | }
594 | }
595 |
596 | function parseErrorMessage(e, defaultMessage = 'Something went wrong') {
597 | if (e.message) { return e.message; }
598 | if (e.result && e.result.error && e.result.error.message) {
599 | return e.result.error.message;
600 | }
601 | return defaultMessage;
602 | }
603 |
--------------------------------------------------------------------------------
/third_party/ng2-codemirror/codemirror.component.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * MIT License, Copyright (c) 2016 Simon Babay and Google.
3 | *
4 | * Source: https://github.com/chymz/ng2-codemirror
5 | */
6 |
7 | import {NgModule, Component, Input, Output, ViewChild, EventEmitter, forwardRef} from '@angular/core';
8 | import {NG_VALUE_ACCESSOR} from '@angular/forms';
9 | import * as CodeMirror from 'codemirror';
10 |
11 | /**
12 | * CodeMirror component
13 | * Usage :
14 | *
15 | */
16 | @Component({
17 | selector: 'codemirror',
18 | providers: [
19 | {
20 | provide: NG_VALUE_ACCESSOR,
21 | useExisting: forwardRef(() => CodemirrorComponent),
22 | multi: true
23 | }
24 | ],
25 | template: ``,
26 | styleUrls: ['./codemirror.component.css']
27 | })
28 | export class CodemirrorComponent {
29 |
30 | @Input() config;
31 |
32 | @Output() change = new EventEmitter();
33 | @Output() query = new EventEmitter();
34 | editor;
35 | @ViewChild('host') host;
36 |
37 | _value = '';
38 | @Output() instance = null;
39 |
40 | /**
41 | * Constructor
42 | */
43 | constructor() {
44 | }
45 |
46 | get value(): any {
47 | return this._value;
48 | };
49 |
50 | @Input() set value(v) {
51 | if (v !== this._value) {
52 | this._value = v;
53 | this.onChange(v);
54 | }
55 | }
56 |
57 | /**
58 | * On component destroy
59 | */
60 | ngOnDestroy() {
61 | }
62 |
63 | /**
64 | * On component view init
65 | */
66 | ngAfterViewInit() {
67 | this.config = this.config || {};
68 | this.codemirrorInit(this.config);
69 | }
70 |
71 | /**
72 | * Initialize codemirror
73 | */
74 | codemirrorInit(config) {
75 | this.instance = CodeMirror.fromTextArea(this.host.nativeElement, config);
76 | this.instance.on('change', () => {
77 | this.updateValue(this.instance.getValue());
78 | });
79 | this.instance.setOption('extraKeys', {
80 | 'Ctrl-Enter': () => {
81 | this.query.emit(true);
82 | }
83 | });
84 | }
85 |
86 | /**
87 | * Value update process
88 | */
89 | updateValue(value) {
90 | this.value = value;
91 | this.onChange(value);
92 | this.onTouched();
93 | this.change.emit(value);
94 | }
95 |
96 | /**
97 | * Implements ControlValueAccessor
98 | */
99 | writeValue(value) {
100 | this._value = value || '';
101 | if (this.instance) {
102 | this.instance.setValue(this._value);
103 | }
104 | }
105 |
106 | onChange(_) {
107 | }
108 |
109 | onTouched() {
110 | }
111 |
112 | registerOnChange(fn) {
113 | this.onChange = fn;
114 | }
115 |
116 | registerOnTouched(fn) {
117 | this.onTouched = fn;
118 | }
119 | }
120 |
121 | CodeMirror.defineMode("sql", function(config, parserConfig) {
122 | "use strict";
123 |
124 | var client = parserConfig.client || {},
125 | atoms = parserConfig.atoms || {"false": true, "true": true, "null": true},
126 | builtin = parserConfig.builtin || {},
127 | keywords = parserConfig.keywords || {},
128 | operatorChars = parserConfig.operatorChars || /^[*+\-%<>!=&|~^]/,
129 | support = parserConfig.support || {},
130 | hooks = parserConfig.hooks || {},
131 | dateSQL = parserConfig.dateSQL || {"date" : true, "time" : true, "timestamp" : true};
132 |
133 | function tokenBase(stream, state) {
134 | var ch = stream.next();
135 |
136 | // call hooks from the mime type
137 | if (hooks[ch]) {
138 | var result = hooks[ch](stream, state);
139 | if (result !== false) return result;
140 | }
141 |
142 | if (support.hexNumber &&
143 | ((ch == "0" && stream.match(/^[xX][0-9a-fA-F]+/))
144 | || (ch == "x" || ch == "X") && stream.match(/^'[0-9a-fA-F]+'/))) {
145 | // hex
146 | // ref: http://dev.mysql.com/doc/refman/5.5/en/hexadecimal-literals.html
147 | return "number";
148 | } else if (support.binaryNumber &&
149 | (((ch == "b" || ch == "B") && stream.match(/^'[01]+'/))
150 | || (ch == "0" && stream.match(/^b[01]+/)))) {
151 | // bitstring
152 | // ref: http://dev.mysql.com/doc/refman/5.5/en/bit-field-literals.html
153 | return "number";
154 | } else if (ch.charCodeAt(0) > 47 && ch.charCodeAt(0) < 58) {
155 | // numbers
156 | // ref: http://dev.mysql.com/doc/refman/5.5/en/number-literals.html
157 | stream.match(/^[0-9]*(\.[0-9]+)?([eE][-+]?[0-9]+)?/);
158 | support.decimallessFloat && stream.match(/^\.(?!\.)/);
159 | return "number";
160 | } else if (ch == "?" && (stream.eatSpace() || stream.eol() || stream.eat(";"))) {
161 | // placeholders
162 | return "variable-3";
163 | } else if (ch == "'" || (ch == '"' && support.doubleQuote)) {
164 | // strings
165 | // ref: http://dev.mysql.com/doc/refman/5.5/en/string-literals.html
166 | state.tokenize = tokenLiteral(ch);
167 | return state.tokenize(stream, state);
168 | } else if ((((support.nCharCast && (ch == "n" || ch == "N"))
169 | || (support.charsetCast && ch == "_" && stream.match(/[a-z][a-z0-9]*/i)))
170 | && (stream.peek() == "'" || stream.peek() == '"'))) {
171 | // charset casting: _utf8'str', N'str', n'str'
172 | // ref: http://dev.mysql.com/doc/refman/5.5/en/string-literals.html
173 | return "keyword";
174 | } else if (/^[\(\),\;\[\]]/.test(ch)) {
175 | // no highlighting
176 | return null;
177 | } else if (support.commentSlashSlash && ch == "/" && stream.eat("/")) {
178 | // 1-line comment
179 | stream.skipToEnd();
180 | return "comment";
181 | } else if ((support.commentHash && ch == "#")
182 | || (ch == "-" && stream.eat("-") && (!support.commentSpaceRequired || stream.eat(" ")))) {
183 | // 1-line comments
184 | // ref: https://kb.askmonty.org/en/comment-syntax/
185 | stream.skipToEnd();
186 | return "comment";
187 | } else if (ch == "/" && stream.eat("*")) {
188 | // multi-line comments
189 | // ref: https://kb.askmonty.org/en/comment-syntax/
190 | state.tokenize = tokenComment(1);
191 | return state.tokenize(stream, state);
192 | } else if (ch == ".") {
193 | // .1 for 0.1
194 | if (support.zerolessFloat && stream.match(/^(?:\d+(?:e[+-]?\d+)?)/i))
195 | return "number";
196 | if (stream.match(/^\.+/))
197 | return null
198 | // .table_name (ODBC)
199 | // // ref: http://dev.mysql.com/doc/refman/5.6/en/identifier-qualifiers.html
200 | if (support.ODBCdotTable && stream.match(/^[\w\d_]+/))
201 | return "variable-2";
202 | } else if (operatorChars.test(ch)) {
203 | // operators
204 | stream.eatWhile(operatorChars);
205 | return null;
206 | } else if (ch == '{' &&
207 | (stream.match(/^( )*(d|D|t|T|ts|TS)( )*'[^']*'( )*}/) || stream.match(/^( )*(d|D|t|T|ts|TS)( )*"[^"]*"( )*}/))) {
208 | // dates (weird ODBC syntax)
209 | // ref: http://dev.mysql.com/doc/refman/5.5/en/date-and-time-literals.html
210 | return "number";
211 | } else {
212 | stream.eatWhile(/^[_\w\d]/);
213 | var word = stream.current().toLowerCase();
214 | // dates (standard SQL syntax)
215 | // ref: http://dev.mysql.com/doc/refman/5.5/en/date-and-time-literals.html
216 | if (dateSQL.hasOwnProperty(word) && (stream.match(/^( )+'[^']*'/) || stream.match(/^( )+"[^"]*"/)))
217 | return "number";
218 | if (atoms.hasOwnProperty(word)) return "atom";
219 | if (builtin.hasOwnProperty(word)) return "builtin";
220 | if (keywords.hasOwnProperty(word)) return "keyword";
221 | if (client.hasOwnProperty(word)) return "string-2";
222 | return null;
223 | }
224 | }
225 |
226 | // 'string', with char specified in quote escaped by '\'
227 | function tokenLiteral(quote) {
228 | return function(stream, state) {
229 | var escaped = false, ch;
230 | while ((ch = stream.next()) != null) {
231 | if (ch == quote && !escaped) {
232 | state.tokenize = tokenBase;
233 | break;
234 | }
235 | escaped = !escaped && ch == "\\";
236 | }
237 | return "string";
238 | };
239 | }
240 | function tokenComment(depth) {
241 | return function(stream, state) {
242 | var m = stream.match(/^.*?(\/\*|\*\/)/)
243 | if (!m) stream.skipToEnd()
244 | else if (m[1] == "/*") state.tokenize = tokenComment(depth + 1)
245 | else if (depth > 1) state.tokenize = tokenComment(depth - 1)
246 | else state.tokenize = tokenBase
247 | return "comment"
248 | }
249 | }
250 |
251 | function pushContext(stream, state, type) {
252 | state.context = {
253 | prev: state.context,
254 | indent: stream.indentation(),
255 | col: stream.column(),
256 | type: type
257 | };
258 | }
259 |
260 | function popContext(state) {
261 | state.indent = state.context.indent;
262 | state.context = state.context.prev;
263 | }
264 |
265 | return {
266 | startState: function() {
267 | return {tokenize: tokenBase, context: null};
268 | },
269 |
270 | token: function(stream, state) {
271 | if (stream.sol()) {
272 | if (state.context && state.context.align == null)
273 | state.context.align = false;
274 | }
275 | if (state.tokenize == tokenBase && stream.eatSpace()) return null;
276 |
277 | var style = state.tokenize(stream, state);
278 | if (style == "comment") return style;
279 |
280 | if (state.context && state.context.align == null)
281 | state.context.align = true;
282 |
283 | var tok = stream.current();
284 | if (tok == "(")
285 | pushContext(stream, state, ")");
286 | else if (tok == "[")
287 | pushContext(stream, state, "]");
288 | else if (state.context && state.context.type == tok)
289 | popContext(state);
290 | return style;
291 | },
292 |
293 | indent: function(state, textAfter) {
294 | var cx = state.context;
295 | if (!cx) return CodeMirror.Pass;
296 | var closing = textAfter.charAt(0) == cx.type;
297 | if (cx.align) return cx.col + (closing ? 0 : 1);
298 | else return cx.indent + (closing ? 0 : config.indentUnit);
299 | },
300 |
301 | blockCommentStart: "/*",
302 | blockCommentEnd: "*/",
303 | lineComment: support.commentSlashSlash ? "//" : support.commentHash ? "#" : "--"
304 | };
305 | });
306 |
307 | (function() {
308 | "use strict";
309 |
310 | // `identifier`
311 | function hookIdentifier(stream) {
312 | // MySQL/MariaDB identifiers
313 | // ref: http://dev.mysql.com/doc/refman/5.6/en/identifier-qualifiers.html
314 | var ch;
315 | while ((ch = stream.next()) != null) {
316 | if (ch == "`" && !stream.eat("`")) return "variable-2";
317 | }
318 | stream.backUp(stream.current().length - 1);
319 | return stream.eatWhile(/\w/) ? "variable-2" : null;
320 | }
321 |
322 | // "identifier"
323 | function hookIdentifierDoublequote(stream) {
324 | // Standard SQL /SQLite identifiers
325 | // ref: http://web.archive.org/web/20160813185132/http://savage.net.au/SQL/sql-99.bnf.html#delimited%20identifier
326 | // ref: http://sqlite.org/lang_keywords.html
327 | var ch;
328 | while ((ch = stream.next()) != null) {
329 | if (ch == "\"" && !stream.eat("\"")) return "variable-2";
330 | }
331 | stream.backUp(stream.current().length - 1);
332 | return stream.eatWhile(/\w/) ? "variable-2" : null;
333 | }
334 |
335 | // variable token
336 | function hookVar(stream) {
337 | // variables
338 | // @@prefix.varName @varName
339 | // varName can be quoted with ` or ' or "
340 | // ref: http://dev.mysql.com/doc/refman/5.5/en/user-variables.html
341 | if (stream.eat("@")) {
342 | stream.match(/^session\./);
343 | stream.match(/^local\./);
344 | stream.match(/^global\./);
345 | }
346 |
347 | if (stream.eat("'")) {
348 | stream.match(/^.*'/);
349 | return "variable-2";
350 | } else if (stream.eat('"')) {
351 | stream.match(/^.*"/);
352 | return "variable-2";
353 | } else if (stream.eat("`")) {
354 | stream.match(/^.*`/);
355 | return "variable-2";
356 | } else if (stream.match(/^[0-9a-zA-Z$\.\_]+/)) {
357 | return "variable-2";
358 | }
359 | return null;
360 | };
361 |
362 | // short client keyword token
363 | function hookClient(stream) {
364 | // \N means NULL
365 | // ref: http://dev.mysql.com/doc/refman/5.5/en/null-values.html
366 | if (stream.eat("N")) {
367 | return "atom";
368 | }
369 | // \g, etc
370 | // ref: http://dev.mysql.com/doc/refman/5.5/en/mysql-commands.html
371 | return stream.match(/^[a-zA-Z.#!?]/) ? "variable-2" : null;
372 | }
373 |
374 | // these keywords are used by all SQL dialects (however, a mode can still overwrite it)
375 | var sqlKeywords = "alter and as asc between by count create delete desc distinct drop from group having in insert into is join like not on or order select set table union update values where limit ";
376 |
377 | // turn a space-separated list into an array
378 | function set(str) {
379 | var obj = {}, words = str.split(" ");
380 | for (var i = 0; i < words.length; ++i) obj[words[i]] = true;
381 | return obj;
382 | }
383 |
384 | // A generic SQL Mode. It's not a standard, it just try to support what is generally supported
385 | CodeMirror.defineMIME("text/x-sql", {
386 | name: "sql",
387 | keywords: set(sqlKeywords + "begin"),
388 | builtin: set("bool boolean bit blob enum long longblob longtext medium mediumblob mediumint mediumtext time timestamp tinyblob tinyint tinytext text bigint int int1 int2 int3 int4 int8 integer float float4 float8 double char varbinary varchar varcharacter precision real date datetime year unsigned signed decimal numeric"),
389 | atoms: set("false true null unknown"),
390 | operatorChars: /^[*+\-%<>!=]/,
391 | dateSQL: set("date time timestamp"),
392 | support: set("ODBCdotTable doubleQuote binaryNumber hexNumber")
393 | });
394 |
395 | CodeMirror.defineMIME("text/x-mssql", {
396 | name: "sql",
397 | client: set("charset clear connect edit ego exit go help nopager notee nowarning pager print prompt quit rehash source status system tee"),
398 | keywords: set(sqlKeywords + "begin trigger proc view index for add constraint key primary foreign collate clustered nonclustered declare exec"),
399 | builtin: set("bigint numeric bit smallint decimal smallmoney int tinyint money float real char varchar text nchar nvarchar ntext binary varbinary image cursor timestamp hierarchyid uniqueidentifier sql_variant xml table "),
400 | atoms: set("false true null unknown"),
401 | operatorChars: /^[*+\-%<>!=]/,
402 | dateSQL: set("date datetimeoffset datetime2 smalldatetime datetime time"),
403 | hooks: {
404 | "@": hookVar
405 | }
406 | });
407 |
408 | CodeMirror.defineMIME("text/x-mysql", {
409 | name: "sql",
410 | client: set("charset clear connect edit ego exit go help nopager notee nowarning pager print prompt quit rehash source status system tee"),
411 | keywords: set(sqlKeywords + "accessible action add after algorithm all analyze asensitive at authors auto_increment autocommit avg avg_row_length before binary binlog both btree cache call cascade cascaded case catalog_name chain change changed character check checkpoint checksum class_origin client_statistics close coalesce code collate collation collations column columns comment commit committed completion concurrent condition connection consistent constraint contains continue contributors convert cross current current_date current_time current_timestamp current_user cursor data database databases day_hour day_microsecond day_minute day_second deallocate dec declare default delay_key_write delayed delimiter des_key_file describe deterministic dev_pop dev_samp deviance diagnostics directory disable discard distinctrow div dual dumpfile each elseif enable enclosed end ends engine engines enum errors escape escaped even event events every execute exists exit explain extended fast fetch field fields first flush for force foreign found_rows full fulltext function general get global grant grants group group_concat handler hash help high_priority hosts hour_microsecond hour_minute hour_second if ignore ignore_server_ids import index index_statistics infile inner innodb inout insensitive insert_method install interval invoker isolation iterate key keys kill language last leading leave left level limit linear lines list load local localtime localtimestamp lock logs low_priority master master_heartbeat_period master_ssl_verify_server_cert masters match max max_rows maxvalue message_text middleint migrate min min_rows minute_microsecond minute_second mod mode modifies modify mutex mysql_errno natural next no no_write_to_binlog offline offset one online open optimize option optionally out outer outfile pack_keys parser partition partitions password phase plugin plugins prepare preserve prev primary privileges procedure processlist profile profiles purge query quick range read read_write reads real rebuild recover references regexp relaylog release remove rename reorganize repair repeatable replace require resignal restrict resume return returns revoke right rlike rollback rollup row row_format rtree savepoint schedule schema schema_name schemas second_microsecond security sensitive separator serializable server session share show signal slave slow smallint snapshot soname spatial specific sql sql_big_result sql_buffer_result sql_cache sql_calc_found_rows sql_no_cache sql_small_result sqlexception sqlstate sqlwarning ssl start starting starts status std stddev stddev_pop stddev_samp storage straight_join subclass_origin sum suspend table_name table_statistics tables tablespace temporary terminated to trailing transaction trigger triggers truncate uncommitted undo uninstall unique unlock upgrade usage use use_frm user user_resources user_statistics using utc_date utc_time utc_timestamp value variables varying view views warnings when while with work write xa xor year_month zerofill begin do then else loop repeat"),
412 | builtin: set("bool boolean bit blob decimal double float long longblob longtext medium mediumblob mediumint mediumtext time timestamp tinyblob tinyint tinytext text bigint int int1 int2 int3 int4 int8 integer float float4 float8 double char varbinary varchar varcharacter precision date datetime year unsigned signed numeric"),
413 | atoms: set("false true null unknown"),
414 | operatorChars: /^[*+\-%<>!=&|^]/,
415 | dateSQL: set("date time timestamp"),
416 | support: set("ODBCdotTable decimallessFloat zerolessFloat binaryNumber hexNumber doubleQuote nCharCast charsetCast commentHash commentSpaceRequired"),
417 | hooks: {
418 | "@": hookVar,
419 | "`": hookIdentifier,
420 | "\\": hookClient
421 | }
422 | });
423 |
424 | CodeMirror.defineMIME("text/x-mariadb", {
425 | name: "sql",
426 | client: set("charset clear connect edit ego exit go help nopager notee nowarning pager print prompt quit rehash source status system tee"),
427 | keywords: set(sqlKeywords + "accessible action add after algorithm all always analyze asensitive at authors auto_increment autocommit avg avg_row_length before binary binlog both btree cache call cascade cascaded case catalog_name chain change changed character check checkpoint checksum class_origin client_statistics close coalesce code collate collation collations column columns comment commit committed completion concurrent condition connection consistent constraint contains continue contributors convert cross current current_date current_time current_timestamp current_user cursor data database databases day_hour day_microsecond day_minute day_second deallocate dec declare default delay_key_write delayed delimiter des_key_file describe deterministic dev_pop dev_samp deviance diagnostics directory disable discard distinctrow div dual dumpfile each elseif enable enclosed end ends engine engines enum errors escape escaped even event events every execute exists exit explain extended fast fetch field fields first flush for force foreign found_rows full fulltext function general generated get global grant grants group groupby_concat handler hard hash help high_priority hosts hour_microsecond hour_minute hour_second if ignore ignore_server_ids import index index_statistics infile inner innodb inout insensitive insert_method install interval invoker isolation iterate key keys kill language last leading leave left level limit linear lines list load local localtime localtimestamp lock logs low_priority master master_heartbeat_period master_ssl_verify_server_cert masters match max max_rows maxvalue message_text middleint migrate min min_rows minute_microsecond minute_second mod mode modifies modify mutex mysql_errno natural next no no_write_to_binlog offline offset one online open optimize option optionally out outer outfile pack_keys parser partition partitions password persistent phase plugin plugins prepare preserve prev primary privileges procedure processlist profile profiles purge query quick range read read_write reads real rebuild recover references regexp relaylog release remove rename reorganize repair repeatable replace require resignal restrict resume return returns revoke right rlike rollback rollup row row_format rtree savepoint schedule schema schema_name schemas second_microsecond security sensitive separator serializable server session share show shutdown signal slave slow smallint snapshot soft soname spatial specific sql sql_big_result sql_buffer_result sql_cache sql_calc_found_rows sql_no_cache sql_small_result sqlexception sqlstate sqlwarning ssl start starting starts status std stddev stddev_pop stddev_samp storage straight_join subclass_origin sum suspend table_name table_statistics tables tablespace temporary terminated to trailing transaction trigger triggers truncate uncommitted undo uninstall unique unlock upgrade usage use use_frm user user_resources user_statistics using utc_date utc_time utc_timestamp value variables varying view views virtual warnings when while with work write xa xor year_month zerofill begin do then else loop repeat"),
428 | builtin: set("bool boolean bit blob decimal double float long longblob longtext medium mediumblob mediumint mediumtext time timestamp tinyblob tinyint tinytext text bigint int int1 int2 int3 int4 int8 integer float float4 float8 double char varbinary varchar varcharacter precision date datetime year unsigned signed numeric"),
429 | atoms: set("false true null unknown"),
430 | operatorChars: /^[*+\-%<>!=&|^]/,
431 | dateSQL: set("date time timestamp"),
432 | support: set("ODBCdotTable decimallessFloat zerolessFloat binaryNumber hexNumber doubleQuote nCharCast charsetCast commentHash commentSpaceRequired"),
433 | hooks: {
434 | "@": hookVar,
435 | "`": hookIdentifier,
436 | "\\": hookClient
437 | }
438 | });
439 |
440 | // provided by the phpLiteAdmin project - phpliteadmin.org
441 | CodeMirror.defineMIME("text/x-sqlite", {
442 | name: "sql",
443 | // commands of the official SQLite client, ref: https://www.sqlite.org/cli.html#dotcmd
444 | client: set("auth backup bail binary changes check clone databases dbinfo dump echo eqp exit explain fullschema headers help import imposter indexes iotrace limit lint load log mode nullvalue once open output print prompt quit read restore save scanstats schema separator session shell show stats system tables testcase timeout timer trace vfsinfo vfslist vfsname width"),
445 | // ref: http://sqlite.org/lang_keywords.html
446 | keywords: set(sqlKeywords + "abort action add after all analyze attach autoincrement before begin cascade case cast check collate column commit conflict constraint cross current_date current_time current_timestamp database default deferrable deferred detach each else end escape except exclusive exists explain fail for foreign full glob if ignore immediate index indexed initially inner instead intersect isnull key left limit match natural no notnull null of offset outer plan pragma primary query raise recursive references regexp reindex release rename replace restrict right rollback row savepoint temp temporary then to transaction trigger unique using vacuum view virtual when with without"),
447 | // SQLite is weakly typed, ref: http://sqlite.org/datatype3.html. This is just a list of some common types.
448 | builtin: set("bool boolean bit blob decimal double float long longblob longtext medium mediumblob mediumint mediumtext time timestamp tinyblob tinyint tinytext text clob bigint int int2 int8 integer float double char varchar date datetime year unsigned signed numeric real"),
449 | // ref: http://sqlite.org/syntax/literal-value.html
450 | atoms: set("null current_date current_time current_timestamp"),
451 | // ref: http://sqlite.org/lang_expr.html#binaryops
452 | operatorChars: /^[*+\-%<>!=&|/~]/,
453 | // SQLite is weakly typed, ref: http://sqlite.org/datatype3.html. This is just a list of some common types.
454 | dateSQL: set("date time timestamp datetime"),
455 | support: set("decimallessFloat zerolessFloat"),
456 | identifierQuote: "\"", //ref: http://sqlite.org/lang_keywords.html
457 | hooks: {
458 | // bind-parameters ref:http://sqlite.org/lang_expr.html#varparam
459 | "@": hookVar,
460 | ":": hookVar,
461 | "?": hookVar,
462 | "$": hookVar,
463 | // The preferred way to escape Identifiers is using double quotes, ref: http://sqlite.org/lang_keywords.html
464 | "\"": hookIdentifierDoublequote,
465 | // there is also support for backtics, ref: http://sqlite.org/lang_keywords.html
466 | "`": hookIdentifier
467 | }
468 | });
469 |
470 | // the query language used by Apache Cassandra is called CQL, but this mime type
471 | // is called Cassandra to avoid confusion with Contextual Query Language
472 | CodeMirror.defineMIME("text/x-cassandra", {
473 | name: "sql",
474 | client: { },
475 | keywords: set("add all allow alter and any apply as asc authorize batch begin by clustering columnfamily compact consistency count create custom delete desc distinct drop each_quorum exists filtering from grant if in index insert into key keyspace keyspaces level limit local_one local_quorum modify nan norecursive nosuperuser not of on one order password permission permissions primary quorum rename revoke schema select set storage superuser table three to token truncate ttl two type unlogged update use user users using values where with writetime"),
476 | builtin: set("ascii bigint blob boolean counter decimal double float frozen inet int list map static text timestamp timeuuid tuple uuid varchar varint"),
477 | atoms: set("false true infinity NaN"),
478 | operatorChars: /^[<>=]/,
479 | dateSQL: { },
480 | support: set("commentSlashSlash decimallessFloat"),
481 | hooks: { }
482 | });
483 |
484 | // this is based on Peter Raganitsch's 'plsql' mode
485 | CodeMirror.defineMIME("text/x-plsql", {
486 | name: "sql",
487 | client: set("appinfo arraysize autocommit autoprint autorecovery autotrace blockterminator break btitle cmdsep colsep compatibility compute concat copycommit copytypecheck define describe echo editfile embedded escape exec execute feedback flagger flush heading headsep instance linesize lno loboffset logsource long longchunksize markup native newpage numformat numwidth pagesize pause pno recsep recsepchar release repfooter repheader serveroutput shiftinout show showmode size spool sqlblanklines sqlcase sqlcode sqlcontinue sqlnumber sqlpluscompatibility sqlprefix sqlprompt sqlterminator suffix tab term termout time timing trimout trimspool ttitle underline verify version wrap"),
488 | keywords: set("abort accept access add all alter and any array arraylen as asc assert assign at attributes audit authorization avg base_table begin between binary_integer body boolean by case cast char char_base check close cluster clusters colauth column comment commit compress connect connected constant constraint crash create current currval cursor data_base database date dba deallocate debugoff debugon decimal declare default definition delay delete desc digits dispose distinct do drop else elseif elsif enable end entry escape exception exception_init exchange exclusive exists exit external fast fetch file for force form from function generic goto grant group having identified if immediate in increment index indexes indicator initial initrans insert interface intersect into is key level library like limited local lock log logging long loop master maxextents maxtrans member minextents minus mislabel mode modify multiset new next no noaudit nocompress nologging noparallel not nowait number_base object of off offline on online only open option or order out package parallel partition pctfree pctincrease pctused pls_integer positive positiven pragma primary prior private privileges procedure public raise range raw read rebuild record ref references refresh release rename replace resource restrict return returning returns reverse revoke rollback row rowid rowlabel rownum rows run savepoint schema segment select separate session set share snapshot some space split sql start statement storage subtype successful synonym tabauth table tables tablespace task terminate then to trigger truncate type union unique unlimited unrecoverable unusable update use using validate value values variable view views when whenever where while with work"),
489 | builtin: set("abs acos add_months ascii asin atan atan2 average bfile bfilename bigserial bit blob ceil character chartorowid chr clob concat convert cos cosh count dec decode deref dual dump dup_val_on_index empty error exp false float floor found glb greatest hextoraw initcap instr instrb int integer isopen last_day least length lengthb ln lower lpad ltrim lub make_ref max min mlslabel mod months_between natural naturaln nchar nclob new_time next_day nextval nls_charset_decl_len nls_charset_id nls_charset_name nls_initcap nls_lower nls_sort nls_upper nlssort no_data_found notfound null number numeric nvarchar2 nvl others power rawtohex real reftohex round rowcount rowidtochar rowtype rpad rtrim serial sign signtype sin sinh smallint soundex sqlcode sqlerrm sqrt stddev string substr substrb sum sysdate tan tanh to_char text to_date to_label to_multi_byte to_number to_single_byte translate true trunc uid unlogged upper user userenv varchar varchar2 variance varying vsize xml"),
490 | operatorChars: /^[*+\-%<>!=~]/,
491 | dateSQL: set("date time timestamp"),
492 | support: set("doubleQuote nCharCast zerolessFloat binaryNumber hexNumber")
493 | });
494 |
495 | // Created to support specific hive keywords
496 | CodeMirror.defineMIME("text/x-hive", {
497 | name: "sql",
498 | keywords: set("select alter $elem$ $key$ $value$ add after all analyze and archive as asc before between binary both bucket buckets by cascade case cast change cluster clustered clusterstatus collection column columns comment compute concatenate continue create cross cursor data database databases dbproperties deferred delete delimited desc describe directory disable distinct distribute drop else enable end escaped exclusive exists explain export extended external false fetch fields fileformat first format formatted from full function functions grant group having hold_ddltime idxproperties if import in index indexes inpath inputdriver inputformat insert intersect into is items join keys lateral left like limit lines load local location lock locks mapjoin materialized minus msck no_drop nocompress not of offline on option or order out outer outputdriver outputformat overwrite partition partitioned partitions percent plus preserve procedure purge range rcfile read readonly reads rebuild recordreader recordwriter recover reduce regexp rename repair replace restrict revoke right rlike row schema schemas semi sequencefile serde serdeproperties set shared show show_database sort sorted ssl statistics stored streamtable table tables tablesample tblproperties temporary terminated textfile then tmp to touch transform trigger true unarchive undo union uniquejoin unlock update use using utc utc_tmestamp view when where while with"),
499 | builtin: set("bool boolean long timestamp tinyint smallint bigint int float double date datetime unsigned string array struct map uniontype"),
500 | atoms: set("false true null unknown"),
501 | operatorChars: /^[*+\-%<>!=]/,
502 | dateSQL: set("date timestamp"),
503 | support: set("ODBCdotTable doubleQuote binaryNumber hexNumber")
504 | });
505 |
506 | CodeMirror.defineMIME("text/x-pgsql", {
507 | name: "sql",
508 | client: set("source"),
509 | // https://www.postgresql.org/docs/10/static/sql-keywords-appendix.html
510 | keywords: set(sqlKeywords + "a abort abs absent absolute access according action ada add admin after aggregate all allocate also always analyse analyze any are array array_agg array_max_cardinality asensitive assertion assignment asymmetric at atomic attribute attributes authorization avg backward base64 before begin begin_frame begin_partition bernoulli binary bit_length blob blocked bom both breadth c cache call called cardinality cascade cascaded case cast catalog catalog_name ceil ceiling chain characteristics characters character_length character_set_catalog character_set_name character_set_schema char_length check checkpoint class class_origin clob close cluster coalesce cobol collate collation collation_catalog collation_name collation_schema collect column columns column_name command_function command_function_code comment comments commit committed concurrently condition condition_number configuration conflict connect connection connection_name constraint constraints constraint_catalog constraint_name constraint_schema constructor contains content continue control conversion convert copy corr corresponding cost covar_pop covar_samp cross csv cube cume_dist current current_catalog current_date current_default_transform_group current_path current_role current_row current_schema current_time current_timestamp current_transform_group_for_type current_user cursor cursor_name cycle data database datalink datetime_interval_code datetime_interval_precision day db deallocate dec declare default defaults deferrable deferred defined definer degree delimiter delimiters dense_rank depth deref derived describe descriptor deterministic diagnostics dictionary disable discard disconnect dispatch dlnewcopy dlpreviouscopy dlurlcomplete dlurlcompleteonly dlurlcompletewrite dlurlpath dlurlpathonly dlurlpathwrite dlurlscheme dlurlserver dlvalue do document domain dynamic dynamic_function dynamic_function_code each element else empty enable encoding encrypted end end-exec end_frame end_partition enforced enum equals escape event every except exception exclude excluding exclusive exec execute exists exp explain expression extension external extract false family fetch file filter final first first_value flag float floor following for force foreign fortran forward found frame_row free freeze fs full function functions fusion g general generated get global go goto grant granted greatest grouping groups handler header hex hierarchy hold hour id identity if ignore ilike immediate immediately immutable implementation implicit import including increment indent index indexes indicator inherit inherits initially inline inner inout input insensitive instance instantiable instead integrity intersect intersection invoker isnull isolation k key key_member key_type label lag language large last last_value lateral lc_collate lc_ctype lead leading leakproof least left length level library like_regex link listen ln load local localtime localtimestamp location locator lock locked logged lower m map mapping match matched materialized max maxvalue max_cardinality member merge message_length message_octet_length message_text method min minute minvalue mod mode modifies module month more move multiset mumps name names namespace national natural nchar nclob nesting new next nfc nfd nfkc nfkd nil no none normalize normalized nothing notify notnull nowait nth_value ntile null nullable nullif nulls number object occurrences_regex octets octet_length of off offset oids old only open operator option options ordering ordinality others out outer output over overlaps overlay overriding owned owner p pad parallel parameter parameter_mode parameter_name parameter_ordinal_position parameter_specific_catalog parameter_specific_name parameter_specific_schema parser partial partition pascal passing passthrough password percent percentile_cont percentile_disc percent_rank period permission placing plans pli policy portion position position_regex power precedes preceding prepare prepared preserve primary prior privileges procedural procedure program public quote range rank read reads reassign recheck recovery recursive ref references referencing refresh regr_avgx regr_avgy regr_count regr_intercept regr_r2 regr_slope regr_sxx regr_sxy regr_syy reindex relative release rename repeatable replace replica requiring reset respect restart restore restrict restricted result return returned_cardinality returned_length returned_octet_length returned_sqlstate returning returns revoke right role rollback rollup routine routine_catalog routine_name routine_schema row rows row_count row_number rule savepoint scale schema schema_name scope scope_catalog scope_name scope_schema scroll search second section security selective self sensitive sequence sequences serializable server server_name session session_user setof sets share show similar simple size skip snapshot some source space specific specifictype specific_name sql sqlcode sqlerror sqlexception sqlstate sqlwarning sqrt stable standalone start state statement static statistics stddev_pop stddev_samp stdin stdout storage strict strip structure style subclass_origin submultiset substring substring_regex succeeds sum symmetric sysid system system_time system_user t tables tablesample tablespace table_name temp template temporary then ties timezone_hour timezone_minute to token top_level_count trailing transaction transactions_committed transactions_rolled_back transaction_active transform transforms translate translate_regex translation treat trigger trigger_catalog trigger_name trigger_schema trim trim_array true truncate trusted type types uescape unbounded uncommitted under unencrypted unique unknown unlink unlisten unlogged unnamed unnest until untyped upper uri usage user user_defined_type_catalog user_defined_type_code user_defined_type_name user_defined_type_schema using vacuum valid validate validator value value_of varbinary variadic var_pop var_samp verbose version versioning view views volatile when whenever whitespace width_bucket window within work wrapper write xmlagg xmlattributes xmlbinary xmlcast xmlcomment xmlconcat xmldeclaration xmldocument xmlelement xmlexists xmlforest xmliterate xmlnamespaces xmlparse xmlpi xmlquery xmlroot xmlschema xmlserialize xmltable xmltext xmlvalidate year yes loop repeat attach path depends detach zone"),
511 | // https://www.postgresql.org/docs/10/static/datatype.html
512 | builtin: set("bigint int8 bigserial serial8 bit varying varbit boolean bool box bytea character char varchar cidr circle date double precision float8 inet integer int int4 interval json jsonb line lseg macaddr macaddr8 money numeric decimal path pg_lsn point polygon real float4 smallint int2 smallserial serial2 serial serial4 text time without zone with timetz timestamp timestamptz tsquery tsvector txid_snapshot uuid xml"),
513 | atoms: set("false true null unknown"),
514 | operatorChars: /^[*+\-%<>!=&|^\/#@?~]/,
515 | dateSQL: set("date time timestamp"),
516 | support: set("ODBCdotTable decimallessFloat zerolessFloat binaryNumber hexNumber nCharCast charsetCast")
517 | });
518 |
519 | // Google's SQL-like query language, GQL
520 | CodeMirror.defineMIME("text/x-gql", {
521 | name: "sql",
522 | keywords: set("ancestor and asc by contains desc descendant distinct from group has in is limit offset on order select superset where"),
523 | atoms: set("false true"),
524 | builtin: set("blob datetime first key __key__ string integer double boolean null"),
525 | operatorChars: /^[*+\-%<>!=]/
526 | });
527 |
528 | // Greenplum
529 | CodeMirror.defineMIME("text/x-gpsql", {
530 | name: "sql",
531 | client: set("source"),
532 | //https://github.com/greenplum-db/gpdb/blob/master/src/include/parser/kwlist.h
533 | keywords: set("abort absolute access action active add admin after aggregate all also alter always analyse analyze and any array as asc assertion assignment asymmetric at authorization backward before begin between bigint binary bit boolean both by cache called cascade cascaded case cast chain char character characteristics check checkpoint class close cluster coalesce codegen collate column comment commit committed concurrency concurrently configuration connection constraint constraints contains content continue conversion copy cost cpu_rate_limit create createdb createexttable createrole createuser cross csv cube current current_catalog current_date current_role current_schema current_time current_timestamp current_user cursor cycle data database day deallocate dec decimal declare decode default defaults deferrable deferred definer delete delimiter delimiters deny desc dictionary disable discard distinct distributed do document domain double drop dxl each else enable encoding encrypted end enum errors escape every except exchange exclude excluding exclusive execute exists explain extension external extract false family fetch fields filespace fill filter first float following for force foreign format forward freeze from full function global grant granted greatest group group_id grouping handler hash having header hold host hour identity if ignore ilike immediate immutable implicit in including inclusive increment index indexes inherit inherits initially inline inner inout input insensitive insert instead int integer intersect interval into invoker is isnull isolation join key language large last leading least left level like limit list listen load local localtime localtimestamp location lock log login mapping master match maxvalue median merge minute minvalue missing mode modifies modify month move name names national natural nchar new newline next no nocreatedb nocreateexttable nocreaterole nocreateuser noinherit nologin none noovercommit nosuperuser not nothing notify notnull nowait null nullif nulls numeric object of off offset oids old on only operator option options or order ordered others out outer over overcommit overlaps overlay owned owner parser partial partition partitions passing password percent percentile_cont percentile_disc placing plans position preceding precision prepare prepared preserve primary prior privileges procedural procedure protocol queue quote randomly range read readable reads real reassign recheck recursive ref references reindex reject relative release rename repeatable replace replica reset resource restart restrict returning returns revoke right role rollback rollup rootpartition row rows rule savepoint scatter schema scroll search second security segment select sequence serializable session session_user set setof sets share show similar simple smallint some split sql stable standalone start statement statistics stdin stdout storage strict strip subpartition subpartitions substring superuser symmetric sysid system table tablespace temp template temporary text then threshold ties time timestamp to trailing transaction treat trigger trim true truncate trusted type unbounded uncommitted unencrypted union unique unknown unlisten until update user using vacuum valid validation validator value values varchar variadic varying verbose version view volatile web when where whitespace window with within without work writable write xml xmlattributes xmlconcat xmlelement xmlexists xmlforest xmlparse xmlpi xmlroot xmlserialize year yes zone"),
534 | builtin: set("bigint int8 bigserial serial8 bit varying varbit boolean bool box bytea character char varchar cidr circle date double precision float float8 inet integer int int4 interval json jsonb line lseg macaddr macaddr8 money numeric decimal path pg_lsn point polygon real float4 smallint int2 smallserial serial2 serial serial4 text time without zone with timetz timestamp timestamptz tsquery tsvector txid_snapshot uuid xml"),
535 | atoms: set("false true null unknown"),
536 | operatorChars: /^[*+\-%<>!=&|^\/#@?~]/,
537 | dateSQL: set("date time timestamp"),
538 | support: set("ODBCdotTable decimallessFloat zerolessFloat binaryNumber hexNumber nCharCast charsetCast")
539 | });
540 |
541 | // Spark SQL
542 | CodeMirror.defineMIME("text/x-sparksql", {
543 | name: "sql",
544 | keywords: set("add after all alter analyze and anti archive array as asc at between bucket buckets by cache cascade case cast change clear cluster clustered codegen collection column columns comment commit compact compactions compute concatenate cost create cross cube current current_date current_timestamp database databases datata dbproperties defined delete delimited desc describe dfs directories distinct distribute drop else end escaped except exchange exists explain export extended external false fields fileformat first following for format formatted from full function functions global grant group grouping having if ignore import in index indexes inner inpath inputformat insert intersect interval into is items join keys last lateral lazy left like limit lines list load local location lock locks logical macro map minus msck natural no not null nulls of on option options or order out outer outputformat over overwrite partition partitioned partitions percent preceding principals purge range recordreader recordwriter recover reduce refresh regexp rename repair replace reset restrict revoke right rlike role roles rollback rollup row rows schema schemas select semi separated serde serdeproperties set sets show skewed sort sorted start statistics stored stratify struct table tables tablesample tblproperties temp temporary terminated then to touch transaction transactions transform true truncate unarchive unbounded uncache union unlock unset use using values view when where window with"),
545 | builtin: set("tinyint smallint int bigint boolean float double string binary timestamp decimal array map struct uniontype delimited serde sequencefile textfile rcfile inputformat outputformat"),
546 | atoms: set("false true null"),
547 | operatorChars: /^[*+\-%<>!=~&|^]/,
548 | dateSQL: set("date time timestamp"),
549 | support: set("ODBCdotTable doubleQuote zerolessFloat")
550 | });
551 |
552 | // Esper
553 | CodeMirror.defineMIME("text/x-esper", {
554 | name: "sql",
555 | client: set("source"),
556 | // http://www.espertech.com/esper/release-5.5.0/esper-reference/html/appendix_keywords.html
557 | keywords: set("alter and as asc between by count create delete desc distinct drop from group having in insert into is join like not on or order select set table union update values where limit after all and as at asc avedev avg between by case cast coalesce count create current_timestamp day days delete define desc distinct else end escape events every exists false first from full group having hour hours in inner insert instanceof into irstream is istream join last lastweekday left limit like max match_recognize matches median measures metadatasql min minute minutes msec millisecond milliseconds not null offset on or order outer output partition pattern prev prior regexp retain-union retain-intersection right rstream sec second seconds select set some snapshot sql stddev sum then true unidirectional until update variable weekday when where window"),
558 | builtin: {},
559 | atoms: set("false true null"),
560 | operatorChars: /^[*+\-%<>!=&|^\/#@?~]/,
561 | dateSQL: set("time"),
562 | support: set("decimallessFloat zerolessFloat binaryNumber hexNumber")
563 | });
564 |
565 | });
566 |
567 | /*
568 | How Properties of Mime Types are used by SQL Mode
569 | =================================================
570 |
571 | keywords:
572 | A list of keywords you want to be highlighted.
573 | builtin:
574 | A list of builtin types you want to be highlighted (if you want types to be of class "builtin" instead of "keyword").
575 | operatorChars:
576 | All characters that must be handled as operators.
577 | client:
578 | Commands parsed and executed by the client (not the server).
579 | support:
580 | A list of supported syntaxes which are not common, but are supported by more than 1 DBMS.
581 | * ODBCdotTable: .tableName
582 | * zerolessFloat: .1
583 | * doubleQuote
584 | * nCharCast: N'string'
585 | * charsetCast: _utf8'string'
586 | * commentHash: use # char for comments
587 | * commentSlashSlash: use // for comments
588 | * commentSpaceRequired: require a space after -- for comments
589 | atoms:
590 | Keywords that must be highlighted as atoms,. Some DBMS's support more atoms than others:
591 | UNKNOWN, INFINITY, UNDERFLOW, NaN...
592 | dateSQL:
593 | Used for date/time SQL standard syntax, because not all DBMS's support same temporal types.
594 | */
595 |
--------------------------------------------------------------------------------