-
Notifications
You must be signed in to change notification settings - Fork 592
/
parse.ts
154 lines (134 loc) · 4.57 KB
/
parse.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
import {SignalRef} from 'vega';
import {hasOwnProperty} from 'vega-util';
import {LATITUDE, LATITUDE2, LONGITUDE, LONGITUDE2, SHAPE} from '../../channel';
import {MAIN} from '../../data';
import {PROJECTION_PROPERTIES} from '../../projection';
import {GEOJSON} from '../../type';
import {duplicate, every, stringify} from '../../util';
import {isUnitModel, Model} from '../model';
import {UnitModel} from '../unit';
import {ProjectionComponent} from './component';
export function parseProjection(model: Model) {
model.component.projection = isUnitModel(model) ? parseUnitProjection(model) : parseNonUnitProjections(model);
}
function parseUnitProjection(model: UnitModel): ProjectionComponent {
if (model.hasProjection) {
const proj = model.specifiedProjection;
const fit = !(proj && (proj.scale != null || proj.translate != null));
const size = fit ? [model.getSizeSignalRef('width'), model.getSizeSignalRef('height')] : undefined;
const data = fit ? gatherFitData(model) : undefined;
return new ProjectionComponent(
model.projectionName(true),
{
...(model.config.projection ?? {}),
...(proj ?? {})
},
size,
data
);
}
return undefined;
}
function gatherFitData(model: UnitModel) {
const data: (SignalRef | string)[] = [];
for (const posssiblePair of [
[LONGITUDE, LATITUDE],
[LONGITUDE2, LATITUDE2]
]) {
if (model.channelHasField(posssiblePair[0]) || model.channelHasField(posssiblePair[1])) {
data.push({
signal: model.getName(`geojson_${data.length}`)
});
}
}
if (model.channelHasField(SHAPE) && model.fieldDef(SHAPE).type === GEOJSON) {
data.push({
signal: model.getName(`geojson_${data.length}`)
});
}
if (data.length === 0) {
// main source is geojson, so we can just use that
data.push(model.requestDataName(MAIN));
}
return data;
}
function mergeIfNoConflict(first: ProjectionComponent, second: ProjectionComponent): ProjectionComponent {
const allPropertiesShared = every(PROJECTION_PROPERTIES, prop => {
// neither has the property
if (!hasOwnProperty(first.explicit, prop) && !hasOwnProperty(second.explicit, prop)) {
return true;
}
// both have property and an equal value for property
if (
hasOwnProperty(first.explicit, prop) &&
hasOwnProperty(second.explicit, prop) &&
// some properties might be signals or objects and require hashing for comparison
stringify(first.get(prop)) === stringify(second.get(prop))
) {
return true;
}
return false;
});
const size = stringify(first.size) === stringify(second.size);
if (size) {
if (allPropertiesShared) {
return first;
} else if (stringify(first.explicit) === stringify({})) {
return second;
} else if (stringify(second.explicit) === stringify({})) {
return first;
}
}
// if all properties don't match, let each unit spec have its own projection
return null;
}
function parseNonUnitProjections(model: Model): ProjectionComponent {
if (model.children.length === 0) {
return undefined;
}
let nonUnitProjection: ProjectionComponent;
// parse all children first
model.children.forEach(child => parseProjection(child));
// analyze parsed projections, attempt to merge
const mergable = every(model.children, child => {
const projection = child.component.projection;
if (!projection) {
// child layer does not use a projection
return true;
} else if (!nonUnitProjection) {
// cached 'projection' is null, cache this one
nonUnitProjection = projection;
return true;
} else {
const merge = mergeIfNoConflict(nonUnitProjection, projection);
if (merge) {
nonUnitProjection = merge;
}
return !!merge;
}
});
// if cached one and all other children share the same projection,
if (nonUnitProjection && mergable) {
// so we can elevate it to the layer level
const name = model.projectionName(true);
const modelProjection = new ProjectionComponent(
name,
nonUnitProjection.specifiedProjection,
nonUnitProjection.size,
duplicate(nonUnitProjection.data)
);
// rename and assign all others as merged
model.children.forEach(child => {
const projection = child.component.projection;
if (projection) {
if (projection.isFit) {
modelProjection.data.push(...child.component.projection.data);
}
child.renameProjection(projection.get('name'), name);
projection.merged = true;
}
});
return modelProjection;
}
return undefined;
}