leaflet.glify
Version:
web gl renderer plugin for leaflet
664 lines (654 loc) • 21.7 kB
text/typescript
import { LatLng, LatLngBounds, Map, Point } from "leaflet";
import { FeatureCollection, Point as GeoPoint } from "geojson";
import { IPointVertex, IPointsSettings, Points } from "./points";
import { ICanvasOverlayDrawEvent } from "./canvas-overlay";
jest.mock("./canvas-overlay");
function getPoints(settings?: Partial<IPointsSettings>): Points {
const element = document.createElement("div");
const map = new Map(element);
const data = [[1, 1]];
return new Points({
size: 5,
map,
data,
vertexShaderSource: " ",
fragmentShaderSource: " ",
latitudeKey: 0,
longitudeKey: 1,
...settings,
});
}
describe("Points", () => {
describe("size", () => {
describe("when this.settings.size is falsey", () => {
it("returns null", () => {
const points = getPoints();
delete points.settings.size;
expect(points.size).toBeNull();
});
});
});
describe("constructor", () => {
let setupSpy: jest.SpyInstance;
let renderSpy: jest.SpyInstance;
beforeEach(() => {
setupSpy = jest.spyOn(Points.prototype, "setup");
renderSpy = jest.spyOn(Points.prototype, "render");
});
afterEach(() => {
setupSpy.mockRestore();
renderSpy.mockRestore();
});
it("sets this.settings", () => {
const points = getPoints();
expect(points.settings.color).toBe(Points.defaults.color);
});
it("sets this.active = true", () => {
const points = getPoints();
expect(points.active).toBe(true);
});
describe("when settings.data is an array", () => {
it('sets this.dataFormat to "Array"', () => {
expect(getPoints({ data: [[1, 1]] }).dataFormat).toEqual("Array");
});
});
describe("when settings.data is a FeatureCollection", () => {
it('sets this.dataFormat to "Array"', () => {
const data: FeatureCollection<GeoPoint> = {
type: "FeatureCollection",
features: [],
};
expect(
getPoints({
data,
}).dataFormat
).toEqual("GeoJson.FeatureCollection");
});
});
describe("when settings.data is unknown", () => {
it("throws", () => {
expect(() => {
getPoints({
data: undefined,
});
}).toThrow();
});
});
describe("when the map projector is not SphericalMercator", () => {
let warnSpy: jest.SpyInstance;
beforeEach(() => {
warnSpy = jest.spyOn(console, "warn");
});
afterEach(() => {
warnSpy.mockRestore();
});
it("warns", () => {
const element = document.createElement("div");
const map = new Map(element);
expect(map.options.crs).not.toBeFalsy();
(map.options.crs ?? { code: "" }).code = "123";
getPoints({ map });
expect(warnSpy).toHaveBeenCalledWith(
"layer designed for SphericalMercator, alternate detected"
);
});
});
it("calls this.setup", () => {
getPoints();
expect(setupSpy).toHaveBeenCalled();
});
it("calls this.render", () => {
getPoints();
expect(renderSpy).toHaveBeenCalled();
});
});
describe("render", () => {
let resetVerticesSpy: jest.SpyInstance;
beforeEach(() => {
resetVerticesSpy = jest.spyOn(Points.prototype, "resetVertices");
});
afterEach(() => {
resetVerticesSpy.mockRestore();
});
it("calls this.resetVertices", () => {
const points = getPoints();
resetVerticesSpy.mockReset();
points.render();
expect(resetVerticesSpy).toHaveBeenCalled();
});
it('sets this.matrix from this.getUniformLocation("matrix")', () => {
const points = getPoints();
points.matrix = null;
points.render();
expect(points.matrix).toBe(points.uniformLocations.matrix);
});
it("sets this.typedVertices correctly", () => {
const points = getPoints();
points.typedVertices = new Float32Array(0);
points.render();
expect(points.typedVertices).toEqual(new Float32Array(points.vertices));
});
it("sets this.mapMatrix size to canvas", () => {
const points = getPoints();
const setSizeSpy = jest.spyOn(points.mapMatrix, "setSize");
points.render();
expect(setSizeSpy).toHaveBeenCalledWith(
points.canvas.width,
points.canvas.height
);
});
it("calls this.gl.viewport correctly", () => {
const points = getPoints();
const viewportSpy = jest.spyOn(points.gl, "viewport");
points.render();
expect(viewportSpy).toHaveBeenCalledWith(
0,
0,
points.canvas.width,
points.canvas.height
);
});
it("calls this.gl.uniformMatrix4fv correctly", () => {
const points = getPoints();
const uniformMatrix4fvSpy = jest.spyOn(points.gl, "uniformMatrix4fv");
points.render();
expect(uniformMatrix4fvSpy).toHaveBeenCalledWith(
points.matrix,
false,
points.mapMatrix.array
);
});
it("calls this.gl.bindBuffer correctly", () => {
const points = getPoints();
const bindBufferSpy = jest.spyOn(points.gl, "bindBuffer");
points.render();
expect(bindBufferSpy).toHaveBeenCalledWith(
points.gl.ARRAY_BUFFER,
points.buffers.vertices
);
});
it("calls this.gl.bufferData correctly", () => {
const points = getPoints();
const bufferDataSpy = jest.spyOn(points.gl, "bufferData");
points.render();
expect(bufferDataSpy).toHaveBeenCalledWith(
points.gl.ARRAY_BUFFER,
points.typedVertices,
points.gl.STATIC_DRAW
);
});
it("calls this.attachShaderVariables correctly", () => {
const points = getPoints();
const attachShaderVariablesSpy = jest.spyOn(
points,
"attachShaderVariables"
);
points.render();
expect(attachShaderVariablesSpy).toHaveBeenCalledWith(4);
});
it("calls layer.redraw", () => {
const points = getPoints();
const redrawSpy = jest.spyOn(points.layer, "redraw");
points.render();
expect(redrawSpy).toHaveBeenCalled();
});
});
describe("getPointLookup", () => {
describe("when key is not defined on this.latLngLookup", () => {
it("sets this.latLngLookup as key and returns defined array", () => {
const points = getPoints();
delete points.latLngLookup.key;
const result = points.getPointLookup("key");
expect(result).toEqual([]);
expect(points.latLngLookup.key).toBe(result);
});
});
describe("when key is defined on this.latLngLookup", () => {
it("returns defined array", () => {
const points = getPoints();
const value = (points.latLngLookup.key = []);
const result = points.getPointLookup("key");
expect(result).toBe(value);
});
});
});
describe("addLookup", () => {
it("pushes lookup to this.latLngLookup and this.allLatLngLookup", () => {
const points = getPoints();
const lookup: IPointVertex = {
latLng: new LatLng(1, 2),
pixel: { x: 0, y: 1 },
chosenColor: {
r: 1,
g: 1,
b: 1,
a: 1,
},
chosenSize: 1,
key: "key",
};
expect(points.latLngLookup.key).toBeUndefined();
points.addLookup(lookup);
expect(points.latLngLookup.key).toContain(lookup);
expect(points.allLatLngLookup).toContain(lookup);
});
});
describe("resetVertices", () => {
it("empties this.latLngLookup", () => {
const points = getPoints();
const oldValue = points.latLngLookup;
points.resetVertices();
expect(points.latLngLookup).not.toBe(oldValue);
});
it("empties this.allLatLngLookup", () => {
const points = getPoints();
const oldValue = points.allLatLngLookup;
points.resetVertices();
expect(points.allLatLngLookup).not.toBe(oldValue);
});
it("empties this.vertices", () => {
const points = getPoints();
const oldValue = points.vertices;
points.resetVertices();
expect(points.vertices).not.toBe(oldValue);
});
describe("when no color is returned", () => {
it("throws", () => {
const points = getPoints();
points.settings.color = null;
expect(() => {
points.resetVertices();
}).toThrow("color is not properly defined");
});
});
describe("when no size is returned", () => {
it("throws", () => {
const points = getPoints();
points.settings.size = null;
expect(() => {
points.resetVertices();
}).toThrow("size is not properly defined");
});
});
describe('when this.dataFormat is "Array"', () => {
describe("when settings.color is a function", () => {
it("calls and uses the result in vertices and projects", () => {
const color = jest.fn((i: number, latlng: LatLng) => {
return {
r: 2,
g: 3,
b: 4,
a: 5,
};
});
const points = getPoints({ data: [[1, 2]], color });
expect(points.dataFormat).toBe("Array");
color.mockClear();
points.resetVertices();
expect(color).toHaveBeenCalledWith(0, new LatLng(1, 2));
expect(points.vertices.slice(2, 6)).toEqual([2, 3, 4, 5]);
});
});
describe("when settings.color is a color", () => {
it("uses the color in vertices and projects", () => {
const color = {
r: 2,
g: 3,
b: 4,
a: 5,
};
const points = getPoints({ data: [[1, 2]], color });
expect(points.dataFormat).toBe("Array");
points.resetVertices();
expect(points.vertices.slice(2, 6)).toEqual([2, 3, 4, 5]);
});
});
describe("when settings.size is a function", () => {
it("calls and uses the result", () => {
const size = jest.fn((i: number, latlng: LatLng | null) => {
return 6;
});
const points = getPoints({ data: [[1, 2]], size });
expect(points.dataFormat).toBe("Array");
size.mockClear();
points.resetVertices();
expect(size).toHaveBeenCalledWith(0, new LatLng(1, 2));
expect(points.vertices[6]).toEqual(6);
});
});
describe("when settings.size is a number", () => {
it("calls and uses the result", () => {
const size = 6;
const points = getPoints({ data: [[1, 2]], size });
expect(points.dataFormat).toBe("Array");
points.resetVertices();
expect(points.vertices[6]).toEqual(6);
});
});
it("projects", () => {
const points = getPoints({ data: [[1, 2]] });
expect(points.dataFormat).toBe("Array");
const project = (points.map.project = jest.fn(
(latlng: LatLng, zoom: number) => {
return new Point(0, 1);
}
));
points.resetVertices();
const expectedLatlng = new LatLng(1, 2);
expect(project).toHaveBeenCalledWith(expectedLatlng, 0);
});
it("calls this.addLookup with vertex", () => {
const color = {
r: 1,
g: 2,
b: 3,
a: 4,
};
const size = 6;
const points = getPoints({ data: [[1, 2]], color, size });
const addLookupSpy = jest.spyOn(points, "addLookup");
points.map.project = jest.fn((latlng: LatLng, zoom: number) => {
return new Point(0, 1);
});
points.resetVertices();
const expected: IPointVertex = {
latLng: new LatLng(1, 2),
key: "1.00x2.00",
pixel: new Point(0, 1),
chosenColor: color,
chosenSize: size,
feature: [1, 2],
};
expect(addLookupSpy).toHaveBeenCalledWith(expected);
});
describe("when this.settings.eachVertex is defined", () => {
it("is called with vertex", () => {
const color = {
r: 1,
g: 2,
b: 3,
a: 4,
};
const size = 6;
const eachVertex = jest.fn((vertex: IPointVertex) => {});
const points = getPoints({ data: [[1, 2]], color, size, eachVertex });
expect(points.dataFormat).toEqual("Array");
points.map.project = jest.fn((latlng: LatLng, zoom: number) => {
return new Point(0, 1);
});
points.resetVertices();
const expected: IPointVertex = {
latLng: new LatLng(1, 2),
key: "1.00x2.00",
pixel: new Point(0, 1),
chosenColor: color,
chosenSize: size,
feature: [1, 2],
};
expect(eachVertex).toHaveBeenCalledWith(expected);
});
});
});
describe('when this.dataFormat is "GeoJson.FeatureCollection"', () => {
const geoJson: FeatureCollection<GeoPoint> = {
type: "FeatureCollection",
features: [
{
type: "Feature",
geometry: {
type: "Point",
coordinates: [1, 2],
},
properties: {
id: 1,
},
},
],
};
describe("when settings.color is a function", () => {
it("calls and uses the result in vertices and projects", () => {
const color = jest.fn((i: number, latlng: LatLng) => {
return {
r: 2,
g: 3,
b: 4,
a: 5,
};
});
const points = getPoints({ data: geoJson, color });
expect(points.dataFormat).toBe("GeoJson.FeatureCollection");
color.mockClear();
points.resetVertices();
expect(color).toHaveBeenCalledWith(0, geoJson.features[0]);
expect(points.vertices.slice(2, 6)).toEqual([2, 3, 4, 5]);
});
});
describe("when settings.color is a color", () => {
it("uses the color in vertices and projects", () => {
const color = {
r: 2,
g: 3,
b: 4,
a: 5,
};
const points = getPoints({ data: geoJson, color });
expect(points.dataFormat).toBe("GeoJson.FeatureCollection");
points.resetVertices();
expect(points.vertices.slice(2, 6)).toEqual([2, 3, 4, 5]);
});
});
describe("when settings.size is a function", () => {
it("calls and uses the result", () => {
const size = jest.fn((i: number, latlng: LatLng | null) => {
return 6;
});
const points = getPoints({ data: geoJson, size });
expect(points.dataFormat).toBe("GeoJson.FeatureCollection");
size.mockClear();
points.resetVertices();
expect(size).toHaveBeenCalledWith(0, new LatLng(1, 2));
expect(points.vertices[6]).toEqual(6);
});
});
describe("when settings.size is a number", () => {
it("calls and uses the result", () => {
const size = 6;
const points = getPoints({ data: geoJson, size });
expect(points.dataFormat).toBe("GeoJson.FeatureCollection");
points.resetVertices();
expect(points.vertices[6]).toEqual(6);
});
});
it("projects", () => {
const points = getPoints({ data: geoJson });
expect(points.dataFormat).toBe("GeoJson.FeatureCollection");
const project = (points.map.project = jest.fn(
(latlng: LatLng, zoom: number) => {
return new Point(0, 1);
}
));
points.resetVertices();
const expectedLatlng = new LatLng(1, 2);
expect(project).toHaveBeenCalledWith(expectedLatlng, 0);
});
it("calls this.addLookup with vertex", () => {
const color = {
r: 1,
g: 2,
b: 3,
a: 4,
};
const size = 6;
const points = getPoints({ data: geoJson, color, size });
expect(points.dataFormat).toEqual("GeoJson.FeatureCollection");
const addLookupSpy = jest.spyOn(points, "addLookup");
points.map.project = jest.fn((latlng: LatLng, zoom: number) => {
return new Point(0, 1);
});
points.resetVertices();
const expected: IPointVertex = {
latLng: new LatLng(1, 2),
key: "1.00x2.00",
pixel: new Point(0, 1),
chosenColor: color,
chosenSize: size,
feature: geoJson.features[0],
};
expect(addLookupSpy).toHaveBeenCalledWith(expected);
});
describe("when this.settings.eachVertex is defined", () => {
it("is called with vertex", () => {
const color = {
r: 1,
g: 2,
b: 3,
a: 4,
};
const size = 6;
const eachVertex = jest.fn((vertex: IPointVertex) => {});
const points = getPoints({ data: geoJson, color, size, eachVertex });
points.map.project = jest.fn((latlng: LatLng, zoom: number) => {
return new Point(0, 1);
});
points.resetVertices();
const expected: IPointVertex = {
latLng: new LatLng(1, 2),
key: "1.00x2.00",
pixel: new Point(0, 1),
chosenColor: color,
chosenSize: size,
feature: geoJson.features[0],
};
expect(eachVertex).toHaveBeenCalledWith(expected);
});
});
});
});
describe("drawOnCanvas", () => {
const event: ICanvasOverlayDrawEvent = {
canvas: document.createElement("canvas"),
bounds: new LatLngBounds(new LatLng(1, 2), new LatLng(1, 2)),
offset: new Point(0, 0),
scale: 1,
size: new Point(1, 1),
zoomScale: 1,
zoom: 1,
};
describe("when this.gl is falsey", () => {
it("returns early", () => {
const points = getPoints();
// @ts-expect-error in case webgl throws or is incompatible
delete points.gl;
expect(points.drawOnCanvas(event)).toEqual(points);
});
});
describe("when this.gl is truthy", () => {
it("calls this.mapMatrix correctly", () => {
const points = getPoints();
const { mapMatrix } = points;
jest.spyOn(points.map, "getZoom").mockReturnValue(1);
const setSizeSpy = jest.spyOn(mapMatrix, "setSize");
const scaleToSpy = jest.spyOn(mapMatrix, "scaleTo");
const translateToSpy = jest.spyOn(mapMatrix, "translateTo");
points.drawOnCanvas(event);
expect(setSizeSpy).toHaveBeenCalledWith(0, 0);
expect(scaleToSpy).toHaveBeenCalledWith(2);
expect(translateToSpy).toHaveBeenCalledWith(-0, -0);
});
it("calls this.gl correctly", () => {
const points = getPoints();
const { gl, canvas, matrix, mapMatrix, allLatLngLookup } = points;
const clearSpy = jest.spyOn(gl, "clear");
const viewportSpy = jest.spyOn(gl, "viewport");
const uniformMatrix4fvSpy = jest.spyOn(gl, "uniformMatrix4fv");
const drawArraysSpy = jest.spyOn(gl, "drawArrays");
points.drawOnCanvas(event);
expect(clearSpy).toHaveBeenCalledWith(gl.COLOR_BUFFER_BIT);
expect(viewportSpy).toHaveBeenCalledWith(
0,
0,
canvas.width,
canvas.height
);
expect(uniformMatrix4fvSpy).toHaveBeenCalledWith(
matrix,
false,
mapMatrix.array
);
expect(drawArraysSpy).toHaveBeenCalledWith(
gl.POINTS,
0,
allLatLngLookup.length
);
});
});
});
describe("lookup", () => {
let closestSpy: jest.SpyInstance;
beforeEach(() => {
closestSpy = jest.spyOn(Points, "closest");
});
describe("when matches", () => {
it("calls Points.closest with coords and matches", () => {
const points = getPoints({ data: [] });
const pointVertex: IPointVertex = {
latLng: new LatLng(0, 0),
pixel: {
x: 0,
y: 0,
},
chosenColor: {
r: 1,
g: 1,
b: 1,
a: 1,
},
chosenSize: 1,
key: "key",
feature: {},
};
points.latLngLookup = {
"-0.03x-0.03": [pointVertex],
};
const coords = new LatLng(0, 0);
points.lookup(coords);
expect(closestSpy).toHaveBeenCalledWith(
coords,
[pointVertex],
points.map
);
});
});
describe("when not matches", () => {
it("calls Points.closest with coords and matches", () => {
const points = getPoints({ data: [] });
const pointVertex: IPointVertex = {
latLng: new LatLng(0, 0),
pixel: {
x: 0,
y: 0,
},
chosenColor: {
r: 1,
g: 1,
b: 1,
a: 1,
},
chosenSize: 1,
key: "key",
feature: {},
};
points.allLatLngLookup.push(pointVertex);
const coords = new LatLng(0, 0);
points.lookup(coords);
expect(closestSpy).toHaveBeenCalledWith(
coords,
points.allLatLngLookup,
points.map
);
});
});
});
// TODO: tryClick
// TODO: tryHover
});