* fix issues:
#16942	Globe Map 1.4.5: If Legend field is not emply, filter working incorrectly
#17076 Color selection for Legend property is not working

* Update packages

Fix bug: If Legend field is not empty, filter working incorrectly

* Fix THREE.WebGELRenderer error

* Fix getting Bing Maps tiles

* Add metadata server
* Update packages
* Remove unused code
* Optimize rendering
* Use Promise-based async operations
* Use static markup for control buttons group

* Fix comments

* Remove empty lines
This commit is contained in:
Konstantin Mamaev 2017-05-18 17:38:25 +03:00 коммит произвёл Avi Sander
Родитель 9929196cc7
Коммит 5852e42c81
19 изменённых файлов: 458 добавлений и 617 удалений

33
.vscode/settings.json поставляемый Normal file
Просмотреть файл

@ -0,0 +1,33 @@
{
"editor.tabSize": 4,
"editor.insertSpaces": true,
"files.eol": "\n",
"files.watcherExclude": {
"**/.git/objects/**": true,
"**/node_modules/**": true,
".tmp": true
},
"files.exclude": {
".tmp": true,
"coverage": true
},
"search.exclude": {
".tmp": true,
"dist": true,
"coverage": true
},
"json.schemas": [
{
"fileMatch": [
"/pbiviz.json"
],
"url": "./.api/v1.6.0/schema.pbiviz.json"
},
{
"fileMatch": [
"/capabilities.json"
],
"url": "./.api/v1.6.0/schema.capabilities.json"
}
]
}

Просмотреть файл

@ -11,8 +11,7 @@
"**/.*", "**/.*",
"node_modules", "node_modules",
"bower_components", "bower_components",
"test", "test"
"tests"
], ],
"dependencies": { "dependencies": {
"webgl-heatmap": "*" "webgl-heatmap": "*"

Просмотреть файл

@ -1,18 +1,17 @@
{ {
"name": "powerbi-visuals-globemap", "name": "powerbi-visuals-globemap",
"description": "GlobeMap", "description": "GlobeMap",
"version": "1.4.3", "version": "1.4.7",
"author": { "author": {
"name": "Microsoft", "name": "Microsoft",
"email": "pbicvsupport@microsoft.com" "email": "pbicvsupport@microsoft.com"
}, },
"scripts": { "scripts": {
"postinstall": "bower install && pbiviz update 1.5.0", "postinstall": "bower install && pbiviz update 1.6.0",
"typings": "typings",
"pbiviz": "pbiviz", "pbiviz": "pbiviz",
"start": "pbiviz start", "start": "pbiviz start",
"package": "pbiviz package", "package": "pbiviz package",
"lint": "node node_modules/tslint/bin/tslint -r \"node_modules/tslint-microsoft-contrib\" \"+(src|test)/**/*.ts\"", "lint": "tslint -r \"node_modules/tslint-microsoft-contrib\" \"+(src|test)/**/*.ts\"",
"pretest": "pbiviz package --resources --no-minify --no-pbiviz", "pretest": "pbiviz package --resources --no-minify --no-pbiviz",
"test": "karma start" "test": "karma start"
}, },
@ -22,36 +21,35 @@
"url": "git+https://github.com/Microsoft/PowerBI-visuals-globemap.git" "url": "git+https://github.com/Microsoft/PowerBI-visuals-globemap.git"
}, },
"dependencies": { "dependencies": {
"globalize": "0.1.0-a2",
"three": "0.75.0",
"lodash": "4.16.2",
"moment": "2.15.1",
"jquery": "3.1.1",
"d3": "3.5.5", "d3": "3.5.5",
"powerbi-visuals-utils-chartutils": "0.2.1", "globalize": "0.1.0-a2",
"powerbi-visuals-utils-colorutils": "0.2.1", "jquery": "3.1.1",
"powerbi-visuals-utils-dataviewutils": "1.0.1" "lodash": "4.17.4",
"powerbi-visuals-utils-colorutils": "0.2.2",
"powerbi-visuals-utils-dataviewutils": "1.1.1",
"powerbi-visuals-utils-formattingutils": "1.0.0",
"powerbi-visuals-utils-interactivityutils": "0.3.1",
"powerbi-visuals-utils-typeutils": "1.0.0",
"three": "0.75.0"
}, },
"devDependencies": { "devDependencies": {
"@types/d3": "3.5.36", "@types/d3": "3.5.36",
"@types/jasmine": "2.5.37", "@types/jasmine": "2.5.47",
"@types/jasmine-jquery": "1.5.28", "@types/jasmine-jquery": "1.5.30",
"@types/jquery": "2.0.41", "@types/jquery": "2.0.41",
"@types/lodash": "4.14.43", "@types/lodash": "4.14.55",
"@types/three": "0.0.19", "@types/three": "0.0.19",
"bingmaps": "1.0.12",
"bower": "1.8.0", "bower": "1.8.0",
"jasmine": "2.5.2", "jasmine": "2.6.0",
"jasmine-jquery": "2.1.1", "jasmine-jquery": "2.1.1",
"karma": "1.3.0", "karma": "1.6.0",
"karma-chrome-launcher": "2.0.0", "karma-chrome-launcher": "2.0.0",
"karma-jasmine": "1.0.2", "karma-jasmine": "1.1.0",
"karma-typescript-preprocessor": "0.3.0", "karma-typescript-preprocessor": "0.3.1",
"powerbi-visuals-tools": "1.5.0", "powerbi-visuals-tools": "1.6.3",
"powerbi-visuals-utils-testutils": "0.2.2", "powerbi-visuals-utils-testutils": "1.0.0",
"powerbi-visuals-utils-typeutils": "^0.2.1", "tslint": "4.5.1",
"tslint": "^4.4.2", "tslint-microsoft-contrib": "^4.0.1",
"tslint-microsoft-contrib": "^4.0.0",
"typescript": "2.1.4" "typescript": "2.1.4"
} }
} }

Просмотреть файл

@ -4,12 +4,12 @@
"displayName": "GlobeMap", "displayName": "GlobeMap",
"guid": "GlobeMap1447669447624", "guid": "GlobeMap1447669447624",
"visualClassName": "GlobeMap", "visualClassName": "GlobeMap",
"version": "1.4.5", "version": "1.4.7",
"description": "A 3D visual using WebGL for plotting locations, with category values displayed as bar heights and heat maps.\n\nShift+Click on bar to change center point. \nSlicing data points will animate to average location.\n\nAttributions:\nthree.js - https://github.com/mrdoob/three.js/\nwebgl-heatmap - https://github.com/pyalot/webgl-heatmap", "description": "A 3D visual using WebGL for plotting locations, with category values displayed as bar heights and heat maps.\n\nShift+Click on bar to change center point. \nSlicing data points will animate to average location.\n\nAttributions:\nthree.js - https://github.com/mrdoob/three.js/\nwebgl-heatmap - https://github.com/pyalot/webgl-heatmap",
"supportUrl": "http://community.powerbi.com", "supportUrl": "http://community.powerbi.com",
"gitHubUrl": "https://github.com/Microsoft/powerbi-visuals-globemap" "gitHubUrl": "https://github.com/Microsoft/powerbi-visuals-globemap"
}, },
"apiVersion": "1.5.0", "apiVersion": "1.6.0",
"author": { "author": {
"name": "Microsoft", "name": "Microsoft",
"email": "pbicvsupport@microsoft.com" "email": "pbicvsupport@microsoft.com"
@ -20,7 +20,7 @@
"externalJS": [ "externalJS": [
"node_modules/jquery/dist/jquery.min.js", "node_modules/jquery/dist/jquery.min.js",
"node_modules/lodash/lodash.min.js", "node_modules/lodash/lodash.min.js",
"node_modules/d3/d3.js", "node_modules/d3/d3.min.js",
"node_modules/three/three.js", "node_modules/three/three.js",
"src/lib/OrbitControls.js", "src/lib/OrbitControls.js",
"node_modules/powerbi-visuals-utils-typeutils/lib/index.js", "node_modules/powerbi-visuals-utils-typeutils/lib/index.js",
@ -30,7 +30,6 @@
"node_modules/powerbi-visuals-utils-dataviewutils/lib/index.js", "node_modules/powerbi-visuals-utils-dataviewutils/lib/index.js",
"node_modules/powerbi-visuals-utils-formattingutils/lib/index.js", "node_modules/powerbi-visuals-utils-formattingutils/lib/index.js",
"node_modules/powerbi-visuals-utils-interactivityutils/lib/index.js", "node_modules/powerbi-visuals-utils-interactivityutils/lib/index.js",
"node_modules/powerbi-visuals-utils-chartutils/lib/index.js",
"node_modules/powerbi-visuals-utils-dataviewutils/lib/index.js", "node_modules/powerbi-visuals-utils-dataviewutils/lib/index.js",
"node_modules/powerbi-visuals-utils-colorutils/lib/index.js", "node_modules/powerbi-visuals-utils-colorutils/lib/index.js",
"bower_components/webgl-heatmap/webgl-heatmap.js" "bower_components/webgl-heatmap/webgl-heatmap.js"

Просмотреть файл

@ -25,165 +25,7 @@
*/ */
namespace powerbi.extensibility.utils { namespace powerbi.extensibility.utils {
import RegExpExtensions = powerbi.extensibility.utils.type.RegExpExtensions;
export module Deprecated {
export const escape: (s: string) => string = window['escape'];
export const unescape: (s: string) => string = window['unescape'];
}
export interface TextMatch {
start: number;
end: number;
text: string;
}
export module UrlUtils { export module UrlUtils {
const urlRegex = /http[s]?:\/\/(\S)+/gi;
export function isValidUrl(value: string): boolean {
if (_.isEmpty(value)) {
return false;
}
let match: RegExpExecArray = RegExpExtensions.run(urlRegex, value);
if (!!match && match.index === 0) {
return true;
}
return false;
}
/* Tests whether a URL is valid.
* @param url The url to be tested.
* @returns Whether the provided url is valid.
**/
export function isValidImageUrl(url: string): boolean {
// For now, passes for any valid Url
return isValidUrl(url);
}
export function findAllValidUrls(text: string): TextMatch[] {
if (_.isEmpty(text)) {
return [];
}
// Find all urls in the text.
// TODO: This could potentially be expensive, maybe include a cap here for text with many urls?
let urlRanges: TextMatch[] = [];
let matches: RegExpExecArray;
let start: number = 0;
while ((matches = RegExpExtensions.run(urlRegex, text, start)) !== null) {
let url: any = matches[0];
let end: number = matches.index + url.length;
urlRanges.push({
start: matches.index,
end: end,
text: url,
});
start = end;
}
return urlRanges;
}
export function isDataUri(uri: string): boolean {
return uri && uri.indexOf('data:') === 0;
}
export function getBase64ContentFromDataUri(uri: string): string {
if (!isDataUri(uri)) {
throw new Error("Expected data uri");
}
// Locate the base 64 content from the URL (e.g. "data:image/png;base64,xxxxx=")
const base64Token = ";base64,";
let indexBase64TokenStart: number = uri.indexOf(base64Token);
if (indexBase64TokenStart < 0) {
throw new Error("Expected base 64 content in data url");
}
let indexBase64Start: number = indexBase64TokenStart + base64Token.length;
return uri.substr(indexBase64Start, uri.length - indexBase64Start);
}
/**
* Create a base64 data URI for a string with a UTF-8 character encoding.
* @param rawText {string} The text string to be encapsulated. It is the raw Javascript string
*/
export function makeUTF8EncodedBase64DataUri(contentType: string, rawText: string): string {
return "data:" + contentType + ";base64," + UrlUtils.utoa(rawText);
}
export function makeJsonDataUri(rawJson: string): string {
return makeUTF8EncodedBase64DataUri("application/json", rawJson);
}
// btoa does not work for char codes > 0xff. for these we have to UTF-8 encode it
// first. cleverly combining the deprecated functions unescape/escape with
// encode/decodeURIComponent gets the browser to do all the work. in case
// unescape/escape are not present, use slower Javascript implementations.
// https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/btoa#Unicode_strings
// http://ecmanaut.blogspot.com/2006/07/encoding-decoding-utf8-in-javascript.html
// http://www.ecma-international.org/publications/files/ECMA-ST-ARCH/ECMA-262,%201st%20edition,%20June%201997.pdf#sec-15.1.2.4
// exported for testing
export function escapeSlow(s: string): string {
if (!s) {
return s;
}
return s.replace(/[^*+\-./0123456789@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz]/g, unescaped => {
let escaped: any = unescaped.charCodeAt(0).toString(16).toUpperCase();
switch (escaped.length) {
case 1: return '%0' + escaped;
case 2: return '%' + escaped;
case 3: return '%u0' + escaped;
default: return '%u' + escaped;
}
});
}
// exported for testing
export function unescapeSlow(s: string): string {
if (!s) {
return s;
}
return s.replace(/%([0-9a-fA-F]{2})|%u([0-9a-fA-F]{4})/g, (_, short, long) => {
return String.fromCharCode(parseInt(short || long, 16));
});
}
const unescape: (s: string) => string = Deprecated.unescape || unescapeSlow;
const escape: (s: string) => string = Deprecated.escape || escapeSlow;
export function encodeUTF8(s: string): string {
return unescape(encodeURIComponent(s));
}
export function decodeUTF8(s: string): string {
return decodeURIComponent(escape(s));
}
export function utoa(s: string): string {
return btoa(encodeUTF8(s));
}
export function atou(s: string): string {
return decodeUTF8(atob(s));
}
/** Returns the set of query parameters in a URL */
export function getQueryParameters(url: string): _.Dictionary<string> {
const query = getQueryString(url);
if (!query) {
return;
}
return parseQueryString(query);
}
/** /**
* Given a URL, set the provided query string parameters * Given a URL, set the provided query string parameters
* @param url The URL to modify * @param url The URL to modify
@ -202,11 +44,11 @@ namespace powerbi.extensibility.utils {
return result; return result;
} }
result += '?' + _.chain(parameters) result += "?" + _.chain(parameters)
.toPairs() .toPairs()
.map(pair => pair.join('=')) .map(pair => pair.join("="))
.value() .value()
.join('&'); .join("&");
return result; return result;
} }
@ -222,57 +64,8 @@ namespace powerbi.extensibility.utils {
}; };
} }
export interface ParsedUrl {
scheme: string;
host: string;
path: string;
query: string;
fragment: string;
}
export function parseUrl(url: string): ParsedUrl {
// see http://www.ietf.org/rfc/rfc3986.txt, Appendix B (around page 50)
// ^(?:([^:/?#]+):)?(?://([^/?#]*))?([^?#]*)(?:\?([^#]*))?(?:#(.*))?
// 1 2 3 4 5
// http://www.ics.uci.edu/pub/ietf/uri/#Related
// scheme = $1 = http
// host = $2 = www.ics.uci.edu
// path = $3 = /pub/ietf/uri/
// query = $4 = <undefined>
// fragment = $5 = Related
let matches: RegExpMatchArray = url.match(/^(?:([^:\/?#]+):)?(?:\/\/([^\/?#]*))?([^?#]*)(?:\?([^#]*))?(?:#(.*))?/);
if (matches) {
return {
scheme: matches[1],
host: matches[2],
path: matches[3],
query: matches[4],
fragment: matches[5]
};
}
}
export function getHost(url: string): string {
let parsed: ParsedUrl = parseUrl(url);
return parsed && parsed.host;
}
const HostnameRegex: any = /https?:\/\/[^\/]+/i;
/**
* Returns everything in a URL after the hostname. Per RFC 3986, this is known as the absolute path reference.
* @example for "https://foo.bar/hello/world", return "/hello/world".
*/
export function getAbsolutePath(url: string): string {
if (!url) {
return url;
}
return url.replace(HostnameRegex, '');
}
function getQueryString(url: string): string { function getQueryString(url: string): string {
let elem: HTMLAnchorElement = document.createElement('a'); let elem: HTMLAnchorElement = document.createElement("a");
elem.href = url; elem.href = url;
return elem.search; return elem.search;
@ -284,7 +77,7 @@ namespace powerbi.extensibility.utils {
return null; return null;
} }
if (_.startsWith(queryString, '?')) { if (_.startsWith(queryString, "?")) {
queryString = queryString.substring(1); queryString = queryString.substring(1);
} }

Просмотреть файл

@ -29,44 +29,12 @@ module powerbi.extensibility.visual {
import DataViewValueColumns = powerbi.DataViewValueColumns; import DataViewValueColumns = powerbi.DataViewValueColumns;
import DataViewCategoricalColumn = powerbi.DataViewCategoricalColumn; import DataViewCategoricalColumn = powerbi.DataViewCategoricalColumn;
import DataViewValueColumn = powerbi.DataViewValueColumn; import DataViewValueColumn = powerbi.DataViewValueColumn;
// powerbi.extensibility.utils.dataview
import converterHelper = powerbi.extensibility.utils.dataview.converterHelper; import converterHelper = powerbi.extensibility.utils.dataview.converterHelper;
export class GlobeMapColumns<T> { export class GlobeMapColumns<T> {
public static getColumnSources(dataView: DataView): GlobeMapColumns<DataViewMetadataColumn> {
return this.getColumnSourcesT<DataViewMetadataColumn>(dataView);
}
public static getTableValues(dataView: DataView): GlobeMapColumns<any> | DataViewTable {
let table: DataViewTable = dataView && dataView.table;
let columns: GlobeMapColumns<any> = this.getColumnSourcesT<any[]>(dataView);
return columns && table && _.mapValues(
columns, (n: DataViewMetadataColumn, i) => n && table.rows.map(row => row[n.index]));
}
public static getTableRows(dataView: DataView): GlobeMapColumns<any> | DataViewTable | GlobeMapColumns<any>[] {
let table: DataViewTable = dataView && dataView.table;
let columns: GlobeMapColumns<any> = this.getColumnSourcesT<any[]>(dataView);
return columns && table && table.rows.map(row =>
_.mapValues(columns, (n: DataViewMetadataColumn, i) => n && row[n.index]));
}
public static getCategoricalValues(dataView: DataView): DataViewCategorical | GlobeMapColumns<any[]> {
let categorical: DataViewCategorical = dataView && dataView.categorical;
let categories: DataViewCategoryColumn[] = categorical && categorical.categories || [];
let values: DataViewValueColumns = categorical && categorical.values || <DataViewValueColumns>[];
let series: DataViewCategorical = categorical && values.source && this.getSeriesValues(dataView);
return categorical && _.mapValues(new this<any[]>(), (n, i) =>
(<DataViewCategoricalColumn[]>_.toArray(categories)).concat(_.toArray(values))
.filter(x => x.source.roles && x.source.roles[i]).map(x => x.values)[0]
|| values.source && values.source.roles && values.source.roles[i] && series);
}
public static getSeriesValues(dataView: DataView) {
return dataView && dataView.categorical && dataView.categorical.values
&& dataView.categorical.values.map(x => converterHelper.getSeriesName(x.source));
}
public static getCategoricalColumns(dataView: DataView): DataViewCategorical | any { public static getCategoricalColumns(dataView: DataView): DataViewCategorical | any {
let categorical = dataView && dataView.categorical; let categorical = dataView && dataView.categorical;
let categories = categorical && categorical.categories || []; let categories = categorical && categorical.categories || [];
@ -87,12 +55,6 @@ module powerbi.extensibility.visual {
(n, i) => g.values.filter(v => v.source.roles[i])[0])); (n, i) => g.values.filter(v => v.source.roles[i])[0]));
} }
private static getColumnSourcesT<T>(dataView: DataView): any {
let columns: any = dataView && dataView.metadata && dataView.metadata.columns;
return columns && _.mapValues(
new this<T>(), (n, i) => columns.filter(x => x.roles && x.roles[i])[0]);
}
public Category: T = null; public Category: T = null;
public Series: T = null; public Series: T = null;
public X: T = null; public X: T = null;

Просмотреть файл

@ -25,7 +25,10 @@
*/ */
module powerbi.extensibility.visual { module powerbi.extensibility.visual {
// powerbi.extensibility.utils.interactivity
import SelectableDataPoint = powerbi.extensibility.utils.interactivity.SelectableDataPoint; import SelectableDataPoint = powerbi.extensibility.utils.interactivity.SelectableDataPoint;
// powerbi.extensibility.geocoder
import ILocation = powerbi.extensibility.geocoder.ILocation; import ILocation = powerbi.extensibility.geocoder.ILocation;
export interface GlobeMapData { export interface GlobeMapData {
@ -52,6 +55,32 @@ module powerbi.extensibility.visual {
color: string; color: string;
category?: string; category?: string;
} }
export interface BingMetadata {
resourceSets: ResourceSet[];
statusCode: string;
statusDescription: string;
}
export interface ResourceSet {
resources: BingResourceMetadata[];
}
export interface BingResourceMetadata {
imageHeight: number;
imageWidth: number;
imageUrl: string;
imageUrlSubdomains: string[];
}
export interface TileMap {
[quadKey: string]: string;
}
export interface CanvasCoordinate {
x: number;
y: number;
}
} }

Просмотреть файл

@ -700,7 +700,6 @@ module powerbi.extensibility.geocoder {
} }
this.activeEntries.push(entry); this.activeEntries.push(entry);
entry.request = $.ajax({ entry.request = $.ajax({
url: url, url: url,
dataType: 'jsonp', dataType: 'jsonp',

Просмотреть файл

@ -1,13 +1,18 @@
module powerbi.extensibility.geocoder { module powerbi.extensibility.geocoder {
import IPromise = powerbi.IPromise; import IPromise = powerbi.IPromise;
import IRect = powerbi.extensibility.utils.svg.IRect;
/** Defines geocoding services. */ /** Defines geocoding services. */
export interface GeocodeOptions { export interface GeocodeOptions {
/** promise that should abort the request when resolved */ /** promise that should abort the request when resolved */
timeout?: IPromise<any>; timeout?: IPromise<any>;
} }
export interface IRect {
left: number;
top: number;
width: number;
height: number;
}
export interface IGeocoder { export interface IGeocoder {
geocode(query: string, category?: string, options?: GeocodeOptions): IPromise<IGeocodeCoordinate>; geocode(query: string, category?: string, options?: GeocodeOptions): IPromise<IGeocodeCoordinate>;
geocodeBoundary(latitude: number, longitude: number, category: string, levelOfDetail?: number, maxGeoData?: number, options?: GeocodeOptions): IPromise<IGeocodeBoundaryCoordinate>; geocodeBoundary(latitude: number, longitude: number, category: string, levelOfDetail?: number, maxGeoData?: number, options?: GeocodeOptions): IPromise<IGeocodeBoundaryCoordinate>;

Просмотреть файл

@ -25,8 +25,9 @@
*/ */
module powerbi.extensibility.geocoder { module powerbi.extensibility.geocoder {
// powerbi.extensibility.utils.formatting
import IStorageService = powerbi.extensibility.utils.formatting.IStorageService; import IStorageService = powerbi.extensibility.utils.formatting.IStorageService;
import LocalStorageService = powerbi.extensibility.utils.formatting.LocalStorageService;
interface GeocodeCacheEntry { interface GeocodeCacheEntry {
coordinate: IGeocodeCoordinate; coordinate: IGeocodeCoordinate;
@ -41,7 +42,7 @@ module powerbi.extensibility.geocoder {
export function createGeocodingCache(maxCacheSize: number, maxCacheSizeOverflow: number, localStorageService?: IStorageService): IGeocodingCache { export function createGeocodingCache(maxCacheSize: number, maxCacheSizeOverflow: number, localStorageService?: IStorageService): IGeocodingCache {
if (!localStorageService) { if (!localStorageService) {
localStorageService = new powerbi.extensibility.utils.formatting.LocalStorageService(); localStorageService = new LocalStorageService();
} }
return new GeocodingCache(maxCacheSize, maxCacheSizeOverflow, localStorageService); return new GeocodingCache(maxCacheSize, maxCacheSizeOverflow, localStorageService);
} }

Просмотреть файл

@ -1,4 +1,4 @@
/* /*
* Power BI Visualizations * Power BI Visualizations
* *
* Copyright (c) Microsoft Corporation * Copyright (c) Microsoft Corporation
@ -24,51 +24,44 @@
* THE SOFTWARE. * THE SOFTWARE.
*/ */
let WebGLHeatmap: any = window['createWebGLHeatmap']; let WebGLHeatmap: any = window["createWebGLHeatmap"];
let GlobeMapCanvasLayers: JQuery[];
module powerbi.extensibility.visual { module powerbi.extensibility.visual {
// powerbi.extensibility.geocoder
import IGeocoder = powerbi.extensibility.geocoder.IGeocoder; import IGeocoder = powerbi.extensibility.geocoder.IGeocoder;
import IGeocodeCoordinate = powerbi.extensibility.geocoder.IGeocodeCoordinate; import IGeocodeCoordinate = powerbi.extensibility.geocoder.IGeocodeCoordinate;
import IPromise = powerbi.IPromise;
import Rectangle = powerbi.extensibility.utils.svg.touch.Rectangle;
import ILocation = powerbi.extensibility.geocoder.ILocation; import ILocation = powerbi.extensibility.geocoder.ILocation;
// powerbi.extensibility.utils.dataview
import converterHelper = powerbi.extensibility.utils.dataview.converterHelper; import converterHelper = powerbi.extensibility.utils.dataview.converterHelper;
// powerbi.extensibility.utils.color
import ColorHelper = powerbi.extensibility.utils.color.ColorHelper; import ColorHelper = powerbi.extensibility.utils.color.ColorHelper;
import ClassAndSelector = powerbi.extensibility.utils.svg.CssConstants.ClassAndSelector; // powerbi.visuals
import createClassAndSelector = powerbi.extensibility.utils.svg.CssConstants.createClassAndSelector; import ISelectionId = powerbi.visuals.ISelectionId;
import DataViewPropertyValue = powerbi.DataViewPropertyValue;
import SelectableDataPoint = powerbi.extensibility.utils.interactivity.SelectableDataPoint; // powerbi.extensibility.utils.formatting
import IValueFormatter = powerbi.extensibility.utils.formatting.IValueFormatter; import IValueFormatter = powerbi.extensibility.utils.formatting.IValueFormatter;
import IInteractivityService = powerbi.extensibility.utils.interactivity.IInteractivityService;
import IMargin = powerbi.extensibility.utils.chart.axis.IMargin;
import IInteractiveBehavior = powerbi.extensibility.utils.interactivity.IInteractiveBehavior;
import ISelectionHandler = powerbi.extensibility.utils.interactivity.ISelectionHandler;
import appendClearCatcher = powerbi.extensibility.utils.interactivity.appendClearCatcher;
import createInteractivityService = powerbi.extensibility.utils.interactivity.createInteractivityService;
import valueFormatter = powerbi.extensibility.utils.formatting.valueFormatter; import valueFormatter = powerbi.extensibility.utils.formatting.valueFormatter;
import IAxisProperties = powerbi.extensibility.utils.chart.axis.IAxisProperties; import LocalStorageService = powerbi.extensibility.utils.formatting.LocalStorageService;
import IVisualHost = powerbi.extensibility.visual.IVisualHost; import IStorageService = powerbi.extensibility.utils.formatting.IStorageService;
import svg = powerbi.extensibility.utils.svg;
import axis = powerbi.extensibility.utils.chart.axis; // powerbi.extensibility.utils.type
import textMeasurementService = powerbi.extensibility.utils.formatting.textMeasurementService; import ValueType = powerbi.extensibility.utils.type.ValueType;
import ValueType = utils.type.ValueType;
import DataViewObjectsParser = utils.dataview.DataViewObjectsParser;
import IColorPalette = powerbi.extensibility.IColorPalette;
interface ExtendedPromise<T> extends IPromise<T> { interface ExtendedPromise<T> extends IPromise<T> {
always(value: any): void; always(value: any): void;
} }
export class GlobeMap implements IVisual { export class GlobeMap implements IVisual {
private localStorageService: IStorageService;
public static MercartorSphere: any; public static MercartorSphere: any;
private static GlobeSettings = { private static GlobeSettings = {
autoRotate: false, autoRotate: false,
earthRadius: 30, earthRadius: 30,
cameraRadius: 100, cameraRadius: 100,
earthSegments: 100, earthSegments: 100,
heatmapSize: 1000, heatmapSize: 1024,
heatPointSize: 7, heatPointSize: 7,
heatIntensity: 10, heatIntensity: 10,
heatmapScaleOnZoom: 0.95, heatmapScaleOnZoom: 0.95,
@ -79,7 +72,13 @@ module powerbi.extensibility.visual {
cameraAnimDuration: 1000, // ms cameraAnimDuration: 1000, // ms
clickInterval: 200 // ms clickInterval: 200 // ms
}; };
private static ChangeDataType: number = 2;
private static ChangeAllType: number = 62;
private static DataPointFillProperty: DataViewObjectPropertyIdentifier = {
objectName: "dataPoint",
propertyName: "fill"
};
private static CountTilesPerSegment: number = 4;
private layout: VisualLayout; private layout: VisualLayout;
private root: JQuery; private root: JQuery;
private rendererContainer: JQuery; private rendererContainer: JQuery;
@ -111,7 +110,6 @@ module powerbi.extensibility.visual {
private hoveredBar: any; private hoveredBar: any;
private averageBarVector: THREE.Vector3; private averageBarVector: THREE.Vector3;
private zoomContainer: d3.Selection<any>; private zoomContainer: d3.Selection<any>;
private zoomControl: d3.Selection<any>;
public colors: IColorPalette; public colors: IColorPalette;
private animationFrameId: number; private animationFrameId: number;
private cameraAnimationFrameId: number; private cameraAnimationFrameId: number;
@ -126,15 +124,13 @@ module powerbi.extensibility.visual {
|| (_.isEmpty(categorical.Height) && _.isEmpty(categorical.Heat))) { || (_.isEmpty(categorical.Height) && _.isEmpty(categorical.Heat))) {
return null; return null;
} }
const properties: GlobeMapSettings = GlobeMapSettings.getDefault() as GlobeMapSettings; const properties: GlobeMapSettings = GlobeMapSettings.getDefault() as GlobeMapSettings;
const settings: GlobeMapSettings = GlobeMap.parseSettings(dataView); const settings: GlobeMapSettings = GlobeMap.parseSettings(dataView);
const groupedColumns: GlobeMapColumns<DataViewValueColumn>[] | any = GlobeMapColumns.getGroupedValueColumns(dataView); const groupedColumns: GlobeMapColumns<DataViewValueColumn>[] | any = GlobeMapColumns.getGroupedValueColumns(dataView);
const dataPoints: any = []; const dataPoints: any = [];
let seriesDataPoints: any = []; let seriesDataPoints: any = [];
let locations: any = []; let locations: any = [];
const colorHelper: ColorHelper = new ColorHelper(colors, null, properties.dataPoint.fill); const colorHelper: ColorHelper = new ColorHelper(colors, GlobeMap.DataPointFillProperty);
let locationType: any; let locationType: any;
let heights: any; let heights: any;
let heightsBySeries: any; let heightsBySeries: any;
@ -158,9 +154,9 @@ module powerbi.extensibility.visual {
// creating a matrix for drawing values by series later. // creating a matrix for drawing values by series later.
for (let i: number = 0; i < groupedColumns.length; i++) { for (let i: number = 0; i < groupedColumns.length; i++) {
const values: any = groupedColumns[i].Height.values; const values: any = groupedColumns[i].Height.values;
seriesDataPoints[i] = GlobeMap.createDataPointForEnumeration( seriesDataPoints[i] = GlobeMap.createDataPointForEnumeration(
dataView, groupedColumns[i].Height.source, i, null, colorHelper, colors, visualHost); dataView, groupedColumns[i].Height.source, i, null, colorHelper, colors, visualHost);
seriesDataPoints[i].color = settings.dataPoint.fill;
for (let j: number = 0; j < values.length; j++) { for (let j: number = 0; j < values.length; j++) {
if (!heights[j]) { if (!heights[j]) {
heights[j] = 0; heights[j] = 0;
@ -192,13 +188,11 @@ module powerbi.extensibility.visual {
heightsBySeries = []; heightsBySeries = [];
seriesDataPoints[0] = GlobeMap.createDataPointForEnumeration( seriesDataPoints[0] = GlobeMap.createDataPointForEnumeration(
dataView, groupedColumns[0].Height.source, 0, dataView.metadata, colorHelper, colors, visualHost); dataView, groupedColumns[0].Height.source, 0, dataView.metadata, colorHelper, colors, visualHost);
seriesDataPoints[0].color = settings.dataPoint.fill;
} }
} else { } else {
heightsBySeries = []; heightsBySeries = [];
heights = []; heights = [];
} }
if (!_.isEmpty(categorical.Heat)) { if (!_.isEmpty(categorical.Heat)) {
if (groupedColumns.length > 1) { if (groupedColumns.length > 1) {
heats = []; heats = [];
@ -262,7 +256,6 @@ module powerbi.extensibility.visual {
dataPoints.push(renderDatum); dataPoints.push(renderDatum);
} }
} }
return { return {
dataView: dataView, dataView: dataView,
dataPoints: dataPoints, dataPoints: dataPoints,
@ -296,19 +289,20 @@ module powerbi.extensibility.visual {
const label: string = valueFormatter.format(nameForFormat, valueFormatter.getFormatString(sourceForFormat, null)); const label: string = valueFormatter.format(nameForFormat, valueFormatter.getFormatString(sourceForFormat, null));
let measureValues = values[0];
const categoryColumn: DataViewCategoryColumn = { const categoryColumn: DataViewCategoryColumn = {
source: values[seriesIndex].source, source: measureValues.source,
values: null, values: null,
identity: [values[seriesIndex].identity] identity: [measureValues.identity]
}; };
const identity: ISelectionId = visualHost.createSelectionIdBuilder() const identity: ISelectionId = visualHost.createSelectionIdBuilder()
.withCategory(categoryColumn, 0) .withCategory(categoryColumn, 0)
.withMeasure(values[seriesIndex].source.queryName) .withMeasure(measureValues.source.queryName)
.createSelectionId(); .createSelectionId();
const category: any = <string>converterHelper.getSeriesName(source); const category: any = <string>converterHelper.getSeriesName(source);
const objects: any = <any>columns.objects; const objects: any = <any>columns.objects || <any>source.objects;
const color: string = objects && objects.dataPoint ? objects.dataPoint.fill.solid.color : metaData && metaData.objects const color: string = objects && objects.dataPoint ? objects.dataPoint.fill.solid.color : metaData && metaData.objects
? colorHelper.getColorForMeasure(metaData.objects, "") ? colorHelper.getColorForMeasure(metaData.objects, "")
: colors.getColor(seriesIndex).value; : colors.getColor(seriesIndex).value;
@ -321,16 +315,47 @@ module powerbi.extensibility.visual {
}; };
} }
private addAnInstanceToEnumeration(
instanceEnumeration: VisualObjectInstanceEnumeration,
instance: VisualObjectInstance): void {
if ((instanceEnumeration as VisualObjectInstanceEnumerationObject).instances) {
(instanceEnumeration as VisualObjectInstanceEnumerationObject)
.instances
.push(instance);
} else {
(instanceEnumeration as VisualObjectInstance[]).push(instance);
}
}
public enumerateObjectInstances(options: EnumerateVisualObjectInstancesOptions): VisualObjectInstance[] | VisualObjectInstanceEnumerationObject { public enumerateObjectInstances(options: EnumerateVisualObjectInstancesOptions): VisualObjectInstance[] | VisualObjectInstanceEnumerationObject {
return GlobeMapSettings.enumerateObjectInstances(this.settings || GlobeMapSettings.getDefault(), options); let instances: VisualObjectInstanceEnumeration = GlobeMapSettings.enumerateObjectInstances(this.settings || GlobeMapSettings.getDefault(), options);
switch (options.objectName) {
case "dataPoint": if (this.data && this.data.seriesDataPoints) {
for (let i: number = 0; i < this.data.seriesDataPoints.length; i++) {
let dataPoint: GlobeMapSeriesDataPoint = this.data.seriesDataPoints[i];
this.addAnInstanceToEnumeration(instances, {
objectName: "dataPoint",
displayName: dataPoint.label,
selector: ColorHelper.normalizeSelector((dataPoint.identity as ISelectionId).getSelector()),
properties: {
fill: { solid: { color: dataPoint.color } }
}
});
}
}
break;
}
return instances;
} }
constructor(options: VisualConstructorOptions) { constructor(options: VisualConstructorOptions) {
this.currentLanguage = options.host.locale;
this.localStorageService = new LocalStorageService();
this.root = $("<div>").appendTo(options.element) this.root = $("<div>").appendTo(options.element)
.attr('drag-resize-disabled', "true") .attr("drag-resize-disabled", "true")
.css({ .css({
'position': "absolute" "position": "absolute"
}); });
this.visualHost = options.host; this.visualHost = options.host;
@ -351,12 +376,16 @@ module powerbi.extensibility.visual {
} }
private setup(): void { private setup(): void {
this.initTextures();
this.initMercartorSphere();
this.initZoomControl();
this.initScene(); this.initScene();
this.initMercartorSphere();
this.initTextures().then(
() => {
this.earth = this.createEarth();
this.scene.add(this.earth);
this.readyToRender = true;
});
this.initZoomControl();
this.initHeatmap(); this.initHeatmap();
this.readyToRender = true;
this.initRayCaster(); this.initRayCaster();
} }
private static cameraFov: number = 35; private static cameraFov: number = 35;
@ -366,6 +395,26 @@ module powerbi.extensibility.visual {
private static ambientLight: number = 0x000000; private static ambientLight: number = 0x000000;
private static directionalLight: number = 0xffffff; private static directionalLight: number = 0xffffff;
private static directionalLightIntensity: number = 0.4; private static directionalLightIntensity: number = 0.4;
private static tileSize: number = 256;
private static maxResolutionLevel: number = 4;
private static metadataUrl: string = `https://dev.virtualearth.net/REST/V1/Imagery/Metadata/Road?output=json&uriScheme=https&key=${powerbi.extensibility.geocoder.Settings.BingKey}`;
private static reserveBindMapsMetadata: BingResourceMetadata = {
imageUrl: "https://{subdomain}.tiles.virtualearth.net/tiles/r{quadkey}.jpeg?g=0&mkt={culture}",
imageUrlSubdomains: [
"t1",
"t2",
"t3",
"t4",
"t5",
"t6",
"t7"
],
imageHeight: 256,
imageWidth: 256
};
private currentLanguage: string = "en-GB";
private static TILE_STORAGE_KEY = "GLOBEMAP_TILES_STORAGE";
private static TILE_LANGUAGE_CULTURE = "GLOBEMAP_TILE_LANGUAGE_CULTURE";
private initScene(): void { private initScene(): void {
this.renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true }); this.renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true });
this.rendererContainer = $("<div>").appendTo(this.root).addClass("globeMapView"); this.rendererContainer = $("<div>").appendTo(this.root).addClass("globeMapView");
@ -390,12 +439,10 @@ module powerbi.extensibility.visual {
const ambientLight: THREE.AmbientLight = new THREE.AmbientLight(GlobeMap.ambientLight); const ambientLight: THREE.AmbientLight = new THREE.AmbientLight(GlobeMap.ambientLight);
const light1: THREE.DirectionalLight = new THREE.DirectionalLight(GlobeMap.directionalLight, GlobeMap.directionalLightIntensity); const light1: THREE.DirectionalLight = new THREE.DirectionalLight(GlobeMap.directionalLight, GlobeMap.directionalLightIntensity);
const light2: THREE.DirectionalLight = new THREE.DirectionalLight(GlobeMap.directionalLight, GlobeMap.directionalLightIntensity); const light2: THREE.DirectionalLight = new THREE.DirectionalLight(GlobeMap.directionalLight, GlobeMap.directionalLightIntensity);
const earth: THREE.Mesh = this.earth = this.createEarth();
this.scene.add(ambientLight); this.scene.add(ambientLight);
this.scene.add(light1); this.scene.add(light1);
this.scene.add(light2); this.scene.add(light2);
this.scene.add(earth);
light1.position.set(20, 20, 20); light1.position.set(20, 20, 20);
light2.position.set(0, 0, -20); light2.position.set(0, 0, -20);
@ -447,7 +494,7 @@ module powerbi.extensibility.visual {
} }
private static dollyX: number = 0.95; private static dollyX: number = 0.95;
public zoomClicked(zoomDirection: any): void { public zoomClicked(zoomDirection: number): void {
if (this.orbitControls.enabled === false) { if (this.orbitControls.enabled === false) {
return; return;
} }
@ -462,7 +509,7 @@ module powerbi.extensibility.visual {
this.animateCamera(this.camera.position); this.animateCamera(this.camera.position);
} }
public rotateCam(deltaX: number, deltaY: number) { public rotateCam(deltaX: number, deltaY: number): void {
if (!this.orbitControls.enabled) { if (!this.orbitControls.enabled) {
return; return;
} }
@ -472,33 +519,145 @@ module powerbi.extensibility.visual {
this.animateCamera(this.camera.position); this.animateCamera(this.camera.position);
} }
private initTextures() { private initTextures(): JQueryPromise<{}> {
if (!GlobeMapCanvasLayers) { this.mapTextures = [];
const tileCulture: string = this.localStorageService.getData(GlobeMap.TILE_LANGUAGE_CULTURE);
let tileCache: TileMap[] = this.localStorageService.getData(GlobeMap.TILE_STORAGE_KEY);
if (!tileCache || tileCulture !== this.currentLanguage) {
// Initialize once, since this is a CPU + Network heavy operation. // Initialize once, since this is a CPU + Network heavy operation.
GlobeMapCanvasLayers = []; return this.getBingMapsServerMetadata()
.then((metadata: BingResourceMetadata) => {
tileCache = [];
let urlTemplate = metadata.imageUrl.replace("{culture}", this.currentLanguage);
for (let level: number = 1; level <= GlobeMap.maxResolutionLevel; ++level) {
let levelTiles = this.generateQuadsByLevel(level, urlTemplate, metadata.imageUrlSubdomains);
this.mapTextures.push(this.createTexture(level, levelTiles));
tileCache.push(levelTiles);
}
this.localStorageService.setData(GlobeMap.TILE_STORAGE_KEY, tileCache);
this.localStorageService.setData(GlobeMap.TILE_LANGUAGE_CULTURE, this.currentLanguage);
return tileCache;
});
} else {
for (let level: number = 1; level <= GlobeMap.maxResolutionLevel; ++level) {
this.mapTextures.push(this.createTexture(level, tileCache[level - 1]));
}
return jQuery.when(tileCache);
}
}
for (let level: number = 2; level <= 5; ++level) { private getBingMapsServerMetadata(): JQueryPromise<BingResourceMetadata> {
const canvas: JQuery = this.getBingMapCanvas(level); return $.ajax(GlobeMap.metadataUrl)
GlobeMapCanvasLayers.push(canvas); .then((data: BingMetadata) => {
if (data.resourceSets.length) {
let resourceSet = data.resourceSets[0];
if (resourceSet && resourceSet.resources.length) {
return resourceSet.resources[0];
}
}
throw "Bing Maps API response was changed. Please update code for new version";
})
.fail((error: any) => {
console.error(JSON.stringify(error));
return GlobeMap.reserveBindMapsMetadata;
});
}
/**
* Generate Bing tile object by map level
* @see https://msdn.microsoft.com/en-us/library/bb259689.aspx
* @private
* @param {number} level map lavel
* @param {string} urlTemplate url template
* @example https://ecn.{subdomain}.tiles.virtualearth.net/tiles/r{quadkey}.jpeg?g=5691&mkt={culture}&shading=hill
* @param {string[]} subdomains list of subdomauns
* @returns {{ [quadKey: string]: string }} Object <quadKey> : <image url>
* @memberOf GlobeMap
*/
private generateQuadsByLevel(level: number, urlTemplate: string, subdomains: string[]): TileMap {
const result: TileMap = {};
let currentSubDomainNumber: number = 0;
const generateQuard = (currentLevel: number = 0, quadKey: string = ""): void => {
if (currentLevel < level) {
for (let i = 0; i < GlobeMap.CountTilesPerSegment; i++) {
generateQuard(currentLevel + 1, `${quadKey}${i}`);
}
} else if (currentLevel === level) {
result[quadKey] = urlTemplate.replace("{subdomain}", subdomains[currentSubDomainNumber]).replace("{quadkey}", quadKey);
currentSubDomainNumber++;
currentSubDomainNumber = currentSubDomainNumber < subdomains.length ? currentSubDomainNumber : 0;
}
};
generateQuard();
return result;
}
private createTexture(level: number, tiles: TileMap): THREE.Texture {
const numSegments: number = Math.pow(2, level);
const canvasSize: number = GlobeMap.tileSize * numSegments;
const canvas: HTMLCanvasElement = document.createElement("canvas");
canvas.width = canvasSize;
canvas.height = canvasSize;
const texture: THREE.Texture = new THREE.Texture(canvas);
texture.needsUpdate = true;
this.loadTiles(canvas, tiles, () => {
texture.needsUpdate = true;
this.needsRender = true;
});
return texture;
}
/**
* Load tiles of Bing Maps
* @param jCanvas jQuery convas object
* @param tiles map of tiles
* @param successCallback call this function when all tiles of the map are successfully loaded
*/
private loadTiles(canvasEl: HTMLCanvasElement, tiles: TileMap, successCallback: Function): void {
let tilesLoaded: number = 0;
const countTiles: number = Object.keys(tiles).length;
const canvasContext: CanvasRenderingContext2D = canvasEl.getContext("2d");
for (let quadKey in tiles) {
if (tiles.hasOwnProperty(quadKey)) {
const coords: any = this.getCoordByQuadKey(quadKey);
const tile: HTMLImageElement = new Image();
tile.onload = (event: Event) => {
tilesLoaded++;
canvasContext.drawImage(tile, coords.x * GlobeMap.tileSize, coords.y * GlobeMap.tileSize, GlobeMap.tileSize, GlobeMap.tileSize);
if (tilesLoaded === countTiles) {
successCallback();
}
};
// So the canvas doesn't get tainted
tile.crossOrigin = "";
tile.src = tiles[quadKey];
}
}
}
/**
* Get coordinates by Bing Maps quard name
* @private
* @param {string} quard Bing Maps quard name
* @returns {CanvasCoordinate} image coordinate
* @memberOf GlobeMap
*/
private getCoordByQuadKey(quard: string): CanvasCoordinate {
const last: number = quard.length - 1;
let x: number = 0;
let y: number = 0;
for (let i: number = last; i >= 0; i--) {
const chr: string = quard.charAt(i);
const pow: number = Math.pow(2, last - i);
switch (chr) {
case "1": x += pow; break;
case "2": y += pow; break;
case "3": x += pow; y += pow; break;
} }
} }
// Can't execute in for loop because variable assignement gets overwritten return { x: x, y: y };
const createTexture: (canvas: JQuery) => THREE.Texture = (canvas: JQuery) => {
const texture: THREE.Texture = new THREE.Texture(<HTMLCanvasElement>canvas.get(0));
texture.needsUpdate = true;
canvas.on("ready", () => {
texture.needsUpdate = true;
this.needsRender = true;
});
return texture;
};
this.mapTextures = [];
for (let i: number = 0; i < GlobeMapCanvasLayers.length; ++i) {
this.mapTextures.push(createTexture(GlobeMapCanvasLayers[i]));
}
} }
private initHeatmap() { private initHeatmap() {
@ -531,19 +690,12 @@ module powerbi.extensibility.visual {
} }
const maxDistance: number = GlobeMap.GlobeSettings.cameraRadius - GlobeMap.GlobeSettings.earthRadius; const maxDistance: number = GlobeMap.GlobeSettings.cameraRadius - GlobeMap.GlobeSettings.earthRadius;
const distance: number = (this.camera.position.length() - GlobeMap.GlobeSettings.earthRadius) / maxDistance; const distance: number = (this.camera.position.length() - GlobeMap.GlobeSettings.earthRadius) / maxDistance;
let texture: THREE.Texture; let texture: THREE.Texture = this.mapTextures[0];
const oneOfFive: number = 1 / 5; for (let divider = 1; divider <= GlobeMap.maxResolutionLevel; divider++) {
const twoOfFive: number = 2 / 5; if (distance <= divider / GlobeMap.maxResolutionLevel) {
const threeOfFive: number = 3 / 5; texture = this.mapTextures[GlobeMap.maxResolutionLevel - divider];
break;
if (distance <= oneOfFive) { }
texture = this.mapTextures[3];
} else if (distance <= twoOfFive) {
texture = this.mapTextures[2];
} else if (distance <= threeOfFive) {
texture = this.mapTextures[1];
} else {
texture = this.mapTextures[0];
} }
if ((<any>this.earth.material).map !== texture) { if ((<any>this.earth.material).map !== texture) {
@ -558,18 +710,15 @@ module powerbi.extensibility.visual {
} }
public update(options: VisualUpdateOptions): void { public update(options: VisualUpdateOptions): void {
if (options.dataViews === undefined || options.dataViews === null) { if (options.dataViews === undefined || options.dataViews === null) {
return; return;
} }
this.layout.viewport = options.viewport; this.layout.viewport = options.viewport;
this.root.css(this.layout.viewportIn); this.root.css(this.layout.viewportIn);
const sixPointsToAdd: number = 6;
this.zoomContainer.style({ this.zoomContainer.style({
'padding-left': (this.layout.viewportIn.width - parseFloat(this.zoomControl.attr("width")) + sixPointsToAdd) + "px", // Fix for chrome "display": this.layout.viewportIn.height > GlobeMap.ZoomControlSettings.height
'display': this.layout.viewportIn.height > $(this.zoomContainer.node()).height() && this.layout.viewportIn.width > GlobeMap.ZoomControlSettings.width
&& this.layout.viewportIn.width > $(this.zoomContainer.node()).width() ? null : "none"
? null : 'none'
}); });
if (this.layout.viewportChanged) { if (this.layout.viewportChanged) {
@ -581,7 +730,7 @@ module powerbi.extensibility.visual {
} }
} }
if (options.type === VisualUpdateType.Data || options.type === VisualUpdateType.All) { if (options.type === GlobeMap.ChangeDataType || options.type === GlobeMap.ChangeAllType) {
this.cleanHeatAndBar(); this.cleanHeatAndBar();
const data: GlobeMapData = GlobeMap.converter(options.dataViews[0], this.colors, this.visualHost); const data: GlobeMapData = GlobeMap.converter(options.dataViews[0], this.colors, this.visualHost);
if (data) { if (data) {
@ -603,9 +752,9 @@ module powerbi.extensibility.visual {
if (!this.data) { if (!this.data) {
return; return;
} }
this.data.dataPoints.forEach(d => this.geocodeRenderDatum(d)); this.data.dataPoints.forEach(d => this.geocodeRenderDatum(d)); // all coordinates (latitude/longitude) will be gained here
this.data.dataPoints.forEach((d) => { this.data.dataPoints.forEach((d) => {
return d.location = d.location || this.globeMapLocationCache[d.placeKey]; return d.location = this.globeMapLocationCache[d.placeKey] || d.location;
}); });
if (!this.readyToRender) { if (!this.readyToRender) {
@ -627,7 +776,9 @@ module powerbi.extensibility.visual {
for (let i: number = 0; i < len; ++i) { for (let i: number = 0; i < len; ++i) {
const renderDatum: GlobeMapDataPoint = this.data.dataPoints[i]; const renderDatum: GlobeMapDataPoint = this.data.dataPoints[i];
if (!renderDatum.location || renderDatum.location.longitude === undefined || renderDatum.location.latitude === undefined) { if (!renderDatum.location || renderDatum.location.longitude === undefined || renderDatum.location.latitude === undefined
|| (renderDatum.location.longitude === 0 && renderDatum.location.latitude === 0)
) {
continue; continue;
} }
@ -658,7 +809,7 @@ module powerbi.extensibility.visual {
const dataPointToolTip: any = []; const dataPointToolTip: any = [];
if (renderDatum.heightBySeries) { if (renderDatum.heightBySeries) {
for (let c: number = 0; c < renderDatum.heightBySeries.length; c++) { for (let c: number = 0; c < renderDatum.heightBySeries.length; c++) {
if (renderDatum.heightBySeries[c]) { if (renderDatum.heightBySeries[c] || renderDatum.heightBySeries[c] === 0) {
measuresBySeries.push(renderDatum.heightBySeries[c]); measuresBySeries.push(renderDatum.heightBySeries[c]);
} }
dataPointToolTip.push(renderDatum.seriesToolTipData[c]); dataPointToolTip.push(renderDatum.seriesToolTipData[c]);
@ -713,7 +864,8 @@ module powerbi.extensibility.visual {
} }
private geocodeRenderDatum(renderDatum: GlobeMapDataPoint) { private geocodeRenderDatum(renderDatum: GlobeMapDataPoint) {
if (renderDatum.location || this.globeMapLocationCache[renderDatum.placeKey]) { // zero valued locations should be updated
if ((renderDatum.location && renderDatum.location.longitude !== 0 && renderDatum.location.latitude !== 0) || this.globeMapLocationCache[renderDatum.placeKey]) {
return; return;
} }
@ -947,7 +1099,7 @@ module powerbi.extensibility.visual {
this.camera = null; this.camera = null;
if (this.renderer) { if (this.renderer) {
if (this.renderer.context) { if (this.renderer.context) {
const extension: any = this.renderer.context.getExtension('WEBGL_lose_context'); const extension: any = this.renderer.context.getExtension("WEBGL_lose_context");
if (extension) { if (extension) {
extension.loseContext(); extension.loseContext();
} }
@ -976,143 +1128,69 @@ module powerbi.extensibility.visual {
this.hideTooltip(); this.hideTooltip();
} }
private static ZoomControlSettings = {
height: 145,
width: 145,
markup: `
<svg width="145" height="145" class="controls">
<g class="control js-control--move-up">
<circle cx="85" cy="20" r="17" />
<path d="M85 8 l12 20 a40,70 0 0,0 -24,0z" />
</g>
<g class="control js-control--move-right">
<circle cx="119" cy="54" r="17" class="zoomControlCircle" />
<path d="M130.9 54 l-20 -12 a70,40 0 0,1 0,24z" class="zoomControlPath" />
</g>
<g class="control js-control--move-down">
<circle cx="85" cy="88" r="17" />
<path d="M 85 100 l12 -20 a40,70 0 0,1 -24,0z" />
</g>
<g class="control js-control--move-left">
<circle cx="51" cy="54" r="17" />
<path d="M39 54 l20 -12 a70,40 0 0,0 0,24z" />
</g>
<g class="control js-control--zoom-down">
<circle cx="51" cy="122" r="17" />
<rect x="42" y="120" width="17" height="6" class="zoomControlPath" />
</g>
<g class="control js-control--zoom-up">
<circle cx="119" cy="122" r="17" />
<rect x="110.5" y="120" width="17" height="6" />
<rect x="116" y="114" width="6" height="17" />
</g>
</svg>
`,
zoomStep: 1,
angleOfRotation: 5
private static zoomControlRatio: number = 8.5; };
private static radiusRatio: number = 3;
private static gapRadiusRatio: number = 2;
private initZoomControl() { private initZoomControl() {
const radius: number = 17; const controlContainer: HTMLElement = document.createElement("div");
const zoomControlWidth: number = radius * GlobeMap.zoomControlRatio; controlContainer.classList.add("controls-container");
const zoomControlHeight: number = radius * GlobeMap.zoomControlRatio; controlContainer.innerHTML = GlobeMap.ZoomControlSettings.markup;
const startX: number = radius * GlobeMap.radiusRatio; this.root.append(controlContainer);
const startY: number = radius + GlobeMap.radiusRatio; function onMouseDown(callback: (element: SVGElement) => void) {
const gap: number = radius * GlobeMap.gapRadiusRatio;
this.zoomContainer = d3.select(this.root[0])
.append('div')
.classed('zoomContainer', true);
this.zoomControl = this.zoomContainer.append("svg").attr({
'width': zoomControlWidth,
'height': zoomControlHeight
}).classed('zoomContainerSvg', true);
const bottom: d3.Selection<any> = this.zoomControl.append("g")
.on("mousedown", () => onMouseDown(() => this.rotateCam(0, -5)));
bottom.append("circle")
.attr({
cx: startX + gap,
cy: startY + (2 * gap),
r: radius
})
.classed('zoomControlCircle', true);
bottom.append("path")
.attr({
d: "M" + (startX + (2 * radius)) + " " + (startY + (radius * 4.7)) + " l12 -20 a40,70 0 0,1 -24,0z",
fill: "gray"
});
const left: d3.Selection<any> = this.zoomControl
.append("g")
.on("mousedown", () => onMouseDown(() => this.rotateCam(5, 0)));
left.append("circle")
.attr({
cx: startX,
cy: startY + gap,
r: radius
})
.classed('zoomControlCircle', true);
left.append("path")
.attr({
d: "M" + (startX - radius / 1.5) + " " + (startY + (radius * 2)) + " l20 -12 a70,40 0 0,0 0,24z"
})
.classed('zoomControlPath', true);
const top: d3.Selection<any> = this.zoomControl
.append("g")
.on("mousedown", () => onMouseDown(() => this.rotateCam(0, 5)));
top
.append("circle").attr({
cx: startX + gap,
cy: startY,
r: radius
})
.classed('zoomControlCircle', true);
top
.append("path").attr({
d: "M" + (startX + (2 * radius)) + " " + (startY - (radius / 1.5)) + " l12 20 a40,70 0 0,0 -24,0z"
}).classed('zoomControlPath', true);
const right: d3.Selection<any> = this.zoomControl
.append("g")
.on("mousedown", () => onMouseDown(() => this.rotateCam(-5, 0)));
right
.append("circle").attr({
cx: startX + (2 * gap),
cy: startY + gap,
r: radius
})
.classed('zoomControlCircle', true);
right
.append("path").attr({
d: "M" + (startX + (4.7 * radius)) + " " + (startY + (radius * 2)) + " l-20 -12 a70,40 0 0,1 0,24z"
}).classed('zoomControlPath', true);
const zoomIn: d3.Selection<any> = this.zoomControl
.append("g")
.on("mousedown", () => onMouseDown(() => this.zoomClicked(-1)));
zoomIn.append("circle")
.attr({
cx: startX + 4 * radius,
cy: startY + 6 * radius,
r: radius
})
.classed('zoomControlCircle', true);
zoomIn.append("rect")
.attr({
x: startX + 3.5 * radius,
y: startY + 5.9 * radius,
width: radius,
height: radius / 3,
fill: "gray"
});
zoomIn.append("rect")
.attr({
x: startX + (4 * radius) - radius / 6,
y: startY + 5.55 * radius,
width: radius / 3,
height: radius
})
.classed('zoomControlPath', true);
const zoomOut: d3.Selection<any> = this.zoomControl
.append("g")
.on("mousedown", () => onMouseDown(() => this.zoomClicked(1)));
zoomOut
.append("circle").attr({
cx: startX,
cy: startY + 6 * radius,
r: radius
})
.classed('zoomControlCircle', true);
zoomOut.append("rect")
.attr({
x: startX - (radius / 2),
y: startY + 5.9 * radius,
width: radius,
height: radius / 3
})
.classed('zoomControlPath', true);
function onMouseDown(callback: () => void) {
(d3.event as MouseEvent).stopPropagation(); (d3.event as MouseEvent).stopPropagation();
if ((<any>d3.event).button === 0) { if ((<any>d3.event).button === 0) {
callback(); callback((<any>d3.event).currentTarget);
} }
} }
this.zoomContainer = d3.select(controlContainer);
this.zoomContainer
.selectAll("g")
.on("mousedown", () => onMouseDown(
(element: SVGElement) => {
const controlType = element.classList.toString().split(" ").filter(className => className.search("js-") !== -1)[0];
switch (controlType) {
case "js-control--move-up": this.rotateCam(0, GlobeMap.ZoomControlSettings.angleOfRotation); break;
case "js-control--move-down": this.rotateCam(0, -GlobeMap.ZoomControlSettings.angleOfRotation); break;
case "js-control--move-left": this.rotateCam(GlobeMap.ZoomControlSettings.angleOfRotation, 0); break;
case "js-control--move-right": this.rotateCam(-GlobeMap.ZoomControlSettings.angleOfRotation, 0); break;
case "js-control--zoom-up": this.zoomClicked(-GlobeMap.ZoomControlSettings.zoomStep); break;
case "js-control--zoom-down": this.zoomClicked(GlobeMap.ZoomControlSettings.zoomStep); break;
}
}));
} }
private initMercartorSphere() { private initMercartorSphere() {
if (GlobeMap.MercartorSphere) return; if (GlobeMap.MercartorSphere) return;
@ -1218,79 +1296,5 @@ module powerbi.extensibility.visual {
MercartorSphere.prototype = Object.create(THREE.Geometry.prototype); MercartorSphere.prototype = Object.create(THREE.Geometry.prototype);
GlobeMap.MercartorSphere = MercartorSphere; GlobeMap.MercartorSphere = MercartorSphere;
} }
private getBingMapCanvas(resolution): JQuery {
const tileSize: number = 256;
let numSegments: number = Math.pow(2, resolution);
let numTiles: number = numSegments * numSegments;
let tilesLoaded: number = 0;
const canvasSize: number = tileSize * numSegments;
const canvas: JQuery = $('<canvas/>').attr({ width: canvasSize, height: canvasSize });
const canvasElem: HTMLCanvasElement = <any>canvas.get(0);
const canvasContext: CanvasRenderingContext2D = canvasElem.getContext("2d");
function generateQuads(res, quad) {
if (res <= resolution) {
if (res === resolution) {
loadTile(quad);
}
generateQuads(res + 1, quad + "0");
generateQuads(res + 1, quad + "1");
generateQuads(res + 1, quad + "2");
generateQuads(res + 1, quad + "3");
}
}
function loadTile(quad) {
const template: any = "https://t{server}.tiles.virtualearth.net/tiles/r{quad}.jpeg?g=0&mkt={language}";
const numServers: number = 7;
const server: number = Math.round(Math.random() * numServers);
const languages: string = "languages";
const language: string = (navigator[languages] && navigator[languages].length) ? navigator[languages][0] : navigator.language;
const url: any = template.replace("{server}", server)
.replace("{quad}", quad)
.replace("{language}", language);
const coords: any = getCoords(quad);
const tile: HTMLImageElement = new Image();
tile.onload = () => {
tilesLoaded++;
canvasContext.drawImage(tile, coords.x * tileSize, coords.y * tileSize, tileSize, tileSize);
if (tilesLoaded === numTiles) {
canvas.trigger("ready", resolution);
}
};
// So the canvas doesn't get tainted
tile.crossOrigin = '';
tile.src = url;
}
function getCoords(quad) {
let x: number = 0;
let y: number = 0;
const last: number = quad.length - 1;
for (let i: number = last; i >= 0; i--) {
const chr: any = quad.charAt(i);
const pow: number = Math.pow(2, last - i);
if (chr === "1") {
x += pow;
} else if (chr === "2") {
y += pow;
} else if (chr === "3") {
x += pow;
y += pow;
}
}
return { x: x, y: y };
}
generateQuads(0, "");
return canvas;
}
} }
} }

Просмотреть файл

@ -25,6 +25,7 @@
*/ */
module powerbi.extensibility.visual { module powerbi.extensibility.visual {
// powerbi.extensibility.utils.dataview
import DataViewObjectsParser = powerbi.extensibility.utils.dataview.DataViewObjectsParser; import DataViewObjectsParser = powerbi.extensibility.utils.dataview.DataViewObjectsParser;
export class GlobeMapSettings extends DataViewObjectsParser { export class GlobeMapSettings extends DataViewObjectsParser {
@ -32,6 +33,5 @@ module powerbi.extensibility.visual {
} }
export class DataPointSettings { export class DataPointSettings {
public fill: string = "#005c55";
} }
} }

Просмотреть файл

@ -25,11 +25,12 @@
*/ */
module powerbi.extensibility.visual { module powerbi.extensibility.visual {
// powerbi export interface IMargin {
import IViewport = powerbi.IViewport; top: number;
bottom: number;
// powerbi.visuals left: number;
import IMargin = powerbi.extensibility.utils.chart.axis.IMargin; right: number;
}
export class VisualLayout { export class VisualLayout {
private marginValue: IMargin; private marginValue: IMargin;

Просмотреть файл

@ -1,26 +1,31 @@
.globeMapView{ .globeMapView {
width: 100%; width: 100%;
height: 100%; height: 100%;
position: relative; position: relative;
} }
.zoomContainer{ .controls-container {
position: absolute; position: fixed;
bottom: -5px; right: 5px;
bottom: 5px;
z-index: 1000; z-index: 1000;
pointer-events: none; pointer-events: none;
} }
.zoomContainerSvg{ .controls {
pointer-events: all; pointer-events: all;
width: 145;
height: 145;
} }
.zoomControlCircle{ .control {
fill: white; circle {
stroke: gray; fill: white;
opacity: 0.5; stroke: gray;
} opacity: 0.5;
}
.zoomControlPath{ path, rect {
fill: gray; fill: gray;
}
} }

Просмотреть файл

@ -29,15 +29,12 @@
/// <reference path="../node_modules/@types/jasmine-jquery/index.d.ts" /> /// <reference path="../node_modules/@types/jasmine-jquery/index.d.ts" />
// Power BI API // Power BI API
/// <reference path="../.api/v1.5.0/PowerBI-visuals.d.ts" /> /// <reference path="../.api/v1.6.0/PowerBI-visuals.d.ts" />
// Power BI Extensibility // Power BI Extensibility
/// <reference path="../node_modules/powerbi-visuals-utils-dataviewutils/lib/index.d.ts" /> /// <reference path="../node_modules/powerbi-visuals-utils-dataviewutils/lib/index.d.ts" />
/// <reference path="../node_modules/powerbi-visuals-utils-typeutils/lib/index.d.ts" /> /// <reference path="../node_modules/powerbi-visuals-utils-typeutils/lib/index.d.ts" />
/// <reference path="../node_modules/powerbi-visuals-utils-svgutils/lib/index.d.ts" />
/// <reference path="../node_modules/powerbi-visuals-utils-interactivityutils/lib/index.d.ts" /> /// <reference path="../node_modules/powerbi-visuals-utils-interactivityutils/lib/index.d.ts" />
/// <reference path="../node_modules/powerbi-visuals-utils-dataviewutils/lib/index.d.ts" />
/// <reference path="../node_modules/powerbi-visuals-utils-formattingutils/lib/index.d.ts" /> /// <reference path="../node_modules/powerbi-visuals-utils-formattingutils/lib/index.d.ts" />
/// <reference path="../node_modules/powerbi-visuals-utils-colorutils/lib/index.d.ts"/> /// <reference path="../node_modules/powerbi-visuals-utils-colorutils/lib/index.d.ts"/>
/// <reference path="../node_modules/powerbi-visuals-utils-testutils/lib/index.d.ts"/> /// <reference path="../node_modules/powerbi-visuals-utils-testutils/lib/index.d.ts"/>

Просмотреть файл

@ -29,16 +29,35 @@
module powerbi.extensibility.visual.test { module powerbi.extensibility.visual.test {
// powerbi.extensibility.utils.test // powerbi.extensibility.utils.test
import VisualBuilderBase = powerbi.extensibility.utils.test.VisualBuilderBase; import VisualBuilderBase = powerbi.extensibility.utils.test.VisualBuilderBase;
import renderTimeout = powerbi.extensibility.utils.test.helpers.renderTimeout;
// GlobeMap1447669447624 // GlobeMap1447669447624
import VisualClass = powerbi.extensibility.visual.GlobeMap1447669447624.GlobeMap; import VisualClass = powerbi.extensibility.visual.GlobeMap1447669447624.GlobeMap;
import VisualPlugin = powerbi.visuals.plugins.GlobeMap1447669447624; import VisualPlugin = powerbi.visuals.plugins.GlobeMap1447669447624;
export class GlobeMapBuilder extends VisualBuilderBase<VisualClass> { export class GlobeMapBuilder extends VisualBuilderBase<VisualClass> {
private static ChangeAllType: number = 62;
constructor(width: number, height: number) { constructor(width: number, height: number) {
super(width, height, VisualPlugin.name); super(width, height, VisualPlugin.name);
} }
public update(dataView: DataView[] | DataView, updateType?: VisualUpdateType): void {
this.visual.update(<VisualUpdateOptions>{
dataViews: _.isArray(dataView) ? dataView : [dataView],
viewport: this.viewport,
type: updateType
});
}
public updateRenderTimeout(
dataViews: DataView[] | DataView,
fn: Function,
updateType: VisualUpdateType = GlobeMapBuilder.ChangeAllType,
timeout?: number): number {
this.update(dataViews, updateType);
return renderTimeout(fn, timeout);
}
protected build(options: any) { protected build(options: any) {
return new VisualClass(options); return new VisualClass(options);
} }

Просмотреть файл

@ -54,7 +54,7 @@ module powerbi.extensibility.visual.test {
beforeEach(() => { beforeEach(() => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
visualBuilder = new GlobeMapBuilder(1000, 500); visualBuilder = new GlobeMapBuilder(1024, 1024);
defaultDataViewBuilder = new GlobeMapDataViewBuilder(); defaultDataViewBuilder = new GlobeMapDataViewBuilder();
dataView = defaultDataViewBuilder.getDataView(); dataView = defaultDataViewBuilder.getDataView();

Просмотреть файл

@ -10,15 +10,13 @@
"declaration": true "declaration": true
}, },
"files": [ "files": [
".api/v1.5.0/PowerBI-visuals.d.ts", ".api/v1.6.0/PowerBI-visuals.d.ts",
"node_modules/powerbi-visuals-utils-formattingutils/lib/index.d.ts", "node_modules/powerbi-visuals-utils-formattingutils/lib/index.d.ts",
"node_modules/powerbi-visuals-utils-interactivityutils/lib/index.d.ts", "node_modules/powerbi-visuals-utils-interactivityutils/lib/index.d.ts",
"node_modules/powerbi-visuals-utils-typeutils/lib/index.d.ts", "node_modules/powerbi-visuals-utils-typeutils/lib/index.d.ts",
"node_modules/powerbi-visuals-utils-svgutils/lib/index.d.ts", "node_modules/powerbi-visuals-utils-svgutils/lib/index.d.ts",
"node_modules/powerbi-visuals-utils-chartutils/lib/index.d.ts",
"node_modules/powerbi-visuals-utils-dataviewutils/lib/index.d.ts", "node_modules/powerbi-visuals-utils-dataviewutils/lib/index.d.ts",
"node_modules/powerbi-visuals-utils-colorutils/lib/index.d.ts", "node_modules/powerbi-visuals-utils-colorutils/lib/index.d.ts",
"node_modules/bingmaps/scripts/MicrosoftMaps/Microsoft.Maps.d.ts",
"src/UrlUtils/UrlUtils.ts", "src/UrlUtils/UrlUtils.ts",
"src/dataInterfaces.ts", "src/dataInterfaces.ts",
"src/settings.ts", "src/settings.ts",

Просмотреть файл

@ -23,7 +23,6 @@
"spaces" "spaces"
], ],
"no-duplicate-variable": true, "no-duplicate-variable": true,
"no-eval": true,
"no-internal-module": false, "no-internal-module": false,
"no-trailing-whitespace": true, "no-trailing-whitespace": true,
"no-unsafe-finally": true, "no-unsafe-finally": true,