namazu-ts 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/maputils.ts CHANGED
@@ -2,13 +2,7 @@ import maplibregl from 'maplibre-gl';
2
2
  import 'maplibre-gl/dist/maplibre-gl.css';
3
3
  import geojson from 'geojson';
4
4
  import { SismoMap } from './sismomap';
5
- import {
6
- SisEvent,
7
- EventFeature,
8
- EventGeoJSON,
9
- EventGeojsonDescriptionProperty,
10
- mapLayers,
11
- } from './types';
5
+ import { SisEvent, EventFeature, EventGeoJSON } from './types';
12
6
  import { isEventGeoJSONProperties } from './utils';
13
7
 
14
8
  /**
@@ -34,6 +28,8 @@ export async function createMap(
34
28
  container: containerID,
35
29
  style: style,
36
30
  center: [0, 0],
31
+ canvasContextAttributes: { antialias: true },
32
+ maxPitch: 60,
37
33
  zoom: 1, // Low value at the start so we get a "zoom in" animation when loading
38
34
  attributionControl: false,
39
35
  });
@@ -47,11 +43,14 @@ export async function createMap(
47
43
 
48
44
  // We use a promise so that people won't use the map before it's fully
49
45
  return new Promise<SismoMap>((resolve) => {
50
- map.once('load', () => {
46
+ smap.map.once('load', () => {
47
+ // We remove these two layers because they're cluttering the visuals
48
+ smap.map.removeLayer('ferry');
49
+ smap.map.removeLayer('boundary_2_maritime');
51
50
  // We use that to have a flat map when zooming
52
51
  // 5 is the value chosen because it covers all the zones of renass.unistra.franceseisme
53
52
  // feel free to change to your liking if needed
54
- map.setProjection({
53
+ smap.map.setProjection({
55
54
  type: ['step', ['zoom'], 'vertical-perspective', 5, 'mercator'],
56
55
  });
57
56
 
@@ -59,39 +58,34 @@ export async function createMap(
59
58
  map.fitBounds(bounds);
60
59
  }
61
60
 
62
- mapLayers.forEach((title: string) => {
63
- if (title != 'stations-layer') {
64
- smap.map.on('mouseenter', title, (e) => {
65
- smap.map.getCanvas().style.cursor = 'pointer';
66
- if (e.features == undefined) return;
67
- if (
68
- !isEventGeoJSONProperties(
69
- e.features[0].properties as any
70
- )
61
+ let titles = ['event-layer', 'station-layer'];
62
+ titles.forEach((title) => {
63
+ smap.map.on('mouseenter', title, (e) => {
64
+ smap.map.getCanvas().style.cursor = 'pointer';
65
+ if (e.features == undefined) return;
66
+ if (
67
+ !isEventGeoJSONProperties(
68
+ e.features[0].properties as any
71
69
  )
72
- return;
73
- let text = JSON.parse(
74
- e.features[0].properties.description
75
- );
76
- console.log(text);
70
+ )
71
+ return;
72
+ let text = JSON.parse(e.features[0].properties.description);
77
73
 
78
- // Pretty ugly but as we know, it'll always be a point, we can do it like this
79
- const feature = e
80
- .features![0] as geojson.Feature<geojson.Point>;
81
- const [lng, lat] = feature.geometry.coordinates;
74
+ // Pretty ugly but as we know, it'll always be a point, we can do it like this
75
+ const feature = e
76
+ .features![0] as geojson.Feature<geojson.Point>;
77
+ const [lng, lat] = feature.geometry.coordinates;
82
78
 
83
- descPopup
84
- .setLngLat([lng, lat])
85
- .setHTML(text[lang])
86
- .addTo(smap.map);
87
- });
88
- smap.map.on('mouseleave', title, () => {
89
- descPopup.remove();
90
- smap.map.getCanvas().style.cursor = '';
91
- });
92
- }
79
+ descPopup
80
+ .setLngLat([lng, lat])
81
+ .setHTML(text[lang])
82
+ .addTo(smap.map);
83
+ });
84
+ smap.map.on('mouseleave', title, () => {
85
+ descPopup.remove();
86
+ smap.map.getCanvas().style.cursor = '';
87
+ });
93
88
  });
94
-
95
89
  resolve(smap);
96
90
  });
97
91
  });
package/src/sismomap.ts CHANGED
@@ -1,14 +1,30 @@
1
1
  import maplibregl from 'maplibre-gl';
2
- import { EventGeoJSON, mapLayers, SisEvent } from './types';
3
- import { isEventGeoJSONProperties, isEventGeoJSON } from './utils';
2
+ import {
3
+ EventGeoJSON,
4
+ EventPhases,
5
+ SisEvent,
6
+ GeoJSON,
7
+ Station,
8
+ StationGeoJSON,
9
+ InfosPhase,
10
+ StationPhases,
11
+ } from './types';
12
+ import { isEventGeoJSON } from './utils';
13
+ import { Client } from './client';
4
14
  import { bbox } from '@turf/turf';
5
15
  import { EventToEventGeoJSON } from './maputils';
16
+ import { ok, err, Result } from 'neverthrow';
17
+ import * as THREE from 'three';
18
+ import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
19
+ import * as MTP from '@dvt3d/maplibre-three-plugin';
6
20
 
7
21
  export class SismoMap {
8
22
  name: string;
9
23
  map: maplibregl.Map;
10
24
  descPopup: maplibregl.Popup;
11
-
25
+ _layers: string[];
26
+ _sources: string[];
27
+ _mapscene: MTP.MapScene;
12
28
  /**
13
29
  * Not meant to use, use createMap instead
14
30
  */
@@ -20,22 +36,149 @@ export class SismoMap {
20
36
  this.name = name;
21
37
  this.map = map;
22
38
  this.descPopup = descPopup;
39
+ this._layers = [];
40
+ this._sources = [];
41
+ this._mapscene = new MTP.MapScene(this.map as any);
42
+
43
+ this._mapscene.addLight(new THREE.AmbientLight(0xffffff, 0.8));
23
44
  }
24
45
 
25
46
  /**
26
47
  * Removes all user-made layers
27
48
  */
28
49
  clear() {
29
- mapLayers.forEach((title) => {
30
- if (this.map.getLayer(title)) {
31
- this.map.removeLayer(title);
50
+ this._layers.forEach((layer: string) => {
51
+ if (this.map.getLayer(layer)) {
52
+ this.map.removeLayer(layer);
32
53
  }
33
- if (this.map.getSource('src')) {
34
- this.map.removeSource('src');
54
+ });
55
+
56
+ this._sources.forEach((source: string) => {
57
+ if (this.map.getSource(source)) {
58
+ this.map.removeSource(source);
35
59
  }
36
60
  });
37
61
  }
38
62
 
63
+ async displayStations(
64
+ client: Client,
65
+ event: SisEvent | EventGeoJSON,
66
+ moveView: boolean = true,
67
+ clear: boolean = true,
68
+ stationOnly: boolean = false,
69
+ model3D: boolean = true
70
+ ) {
71
+ let eventGeo: EventGeoJSON;
72
+ // Type guard to distincting the overloading cases
73
+ if (isEventGeoJSON(event)) {
74
+ eventGeo = event;
75
+ } else {
76
+ eventGeo = EventToEventGeoJSON(event);
77
+ }
78
+
79
+ if (eventGeo.features.length != 1) {
80
+ return Promise.reject(
81
+ Error(
82
+ 'If passing a EventGeoJSON, it should only have one feature'
83
+ )
84
+ );
85
+ }
86
+
87
+ if (!stationOnly) this.displayEvents(eventGeo, false, clear);
88
+
89
+ // I put the await later because it doesn't have dependencies when creating the station,string map
90
+ let phasesPromise = client.getEventPhases(eventGeo);
91
+ let zone = await client.getZoneNameByEvent(eventGeo);
92
+ let stationList = await client.getStationsByZoneName(zone);
93
+
94
+ // We create a map with the stations so our search is o(1) with just a o(n) to create the map
95
+ let stationsMap: Map<string, StationPhases> = new Map<
96
+ string,
97
+ StationPhases
98
+ >();
99
+ stationList.features.forEach((station: Station) => {
100
+ let stationPhase: StationPhases = station as StationPhases;
101
+ stationPhase.properties.phases = [];
102
+ stationsMap.set(stationPhase.properties.stationcode, stationPhase);
103
+ });
104
+
105
+ let phases: EventPhases = await phasesPromise;
106
+ let absentStations: InfosPhase[] = [];
107
+ let stationsGeoJSON: StationGeoJSON = {
108
+ type: 'FeatureCollection',
109
+ features: [],
110
+ };
111
+
112
+ phases.forEach((phase: InfosPhase) => {
113
+ let station = stationsMap.get(phase.stationCode);
114
+ if (station == undefined) {
115
+ absentStations.push(phase);
116
+ return;
117
+ }
118
+ station?.properties.phases.push(phase);
119
+ });
120
+
121
+ stationsMap.forEach((station) => {
122
+ if (station.properties.phases.length == 0) return;
123
+ station.properties.description = {
124
+ fr: station.properties.stationcode,
125
+ en: station.properties.stationcode,
126
+ };
127
+ stationsGeoJSON.features.push(station);
128
+ });
129
+
130
+ console.log('Absent stations : ');
131
+ console.log(absentStations);
132
+
133
+ if (model3D) {
134
+ stationsGeoJSON.features.forEach((station: StationPhases) => {
135
+ this.addStation(station.geometry.coordinates);
136
+ });
137
+ if (moveView) {
138
+ this.centerView(stationsGeoJSON);
139
+ }
140
+ } else {
141
+ const svgImage = new Image(20, 20);
142
+ const triangleSVG = `
143
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
144
+ <polygon points="12,2 22,22 2,22" fill="blue" />
145
+ </svg>`;
146
+
147
+ const encodedSVG = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(triangleSVG)}`;
148
+
149
+ svgImage.src = encodedSVG;
150
+
151
+ svgImage.onload = () => {
152
+ this.map.addImage('triangle', svgImage);
153
+ };
154
+
155
+ let layerSpec: maplibregl.AddLayerObject = {
156
+ id: 'station-layer',
157
+ source: 'stat',
158
+
159
+ type: 'symbol',
160
+ layout: {
161
+ 'icon-image': 'triangle',
162
+ 'icon-allow-overlap': true,
163
+ 'icon-size': 0.8,
164
+ },
165
+ };
166
+
167
+ if (
168
+ this.displayGeoJSON(
169
+ stationsGeoJSON,
170
+ 'stat',
171
+ layerSpec,
172
+ moveView,
173
+ false
174
+ ).isErr()
175
+ )
176
+ return Promise.reject();
177
+ }
178
+
179
+ return Promise.resolve();
180
+ }
181
+
39
182
  /**
40
183
  * @param eventOrList Can either be a single Event (via /events API) or a FDSN GeoJSON
41
184
  * @param moveView True = The view moves on the events' bounding box while displaying (does not affect user's possible behaviour on the map)
@@ -48,21 +191,144 @@ export class SismoMap {
48
191
  ) {
49
192
  let eventList: EventGeoJSON;
50
193
 
51
- // Type guard to disting the overloading cases
194
+ // Type guard to distincting the overloading cases
52
195
  if (isEventGeoJSON(eventOrList)) {
53
196
  eventList = eventOrList;
54
197
  } else {
55
198
  eventList = EventToEventGeoJSON(eventOrList as SisEvent);
56
199
  }
200
+ let layerSpec: maplibregl.AddLayerObject = {
201
+ id: 'event-layer',
202
+ source: 'src',
203
+ type: 'circle',
204
+ paint: {
205
+ // Inspired by https://renass.unistra.fr/js/events.js
206
+ 'circle-radius': ['*', 8, ['ln', ['+', 1, ['get', 'mag']]]],
207
+ 'circle-color': '#B42222',
208
+ 'circle-opacity': 0.8,
209
+ 'circle-stroke-width': 1,
210
+ 'circle-stroke-opacity': 0.9,
211
+ },
212
+ };
213
+
214
+ this.displayGeoJSON(eventList, 'src', layerSpec, moveView, clear);
215
+ }
216
+
217
+ // Updates a source by appending new data
218
+ // Take care to add the good type, dangerous function and should be
219
+ private updateSource(sourceName: string, newData: GeoJSON) {
220
+ let source: maplibregl.GeoJSONSource | undefined =
221
+ this.map.getSource(sourceName);
222
+ let data = source?._data.geojson;
223
+ if (data === undefined) return;
224
+ let geojson: GeoJSON = data as GeoJSON;
225
+
226
+ geojson.features = (geojson.features as any).concat(newData.features);
227
+ source?.setData(geojson as any); // It's good but can't do better as stated in line 104
228
+ }
229
+
230
+ private centerView(geojson: GeoJSON) {
231
+ if (geojson.features.length == 1) {
232
+ let lng: number = geojson.features[0].geometry.coordinates[0];
233
+ let lat: number = geojson.features[0].geometry.coordinates[1];
234
+ this.map.flyTo({ center: [lng, lat], zoom: 6 });
235
+ } else {
236
+ // Inter-library operability is annoying, this line will always work with the types i'll give it
237
+ // but i'm sorry for the double cast as any, can't do better :(
238
+ let bounds = bbox(geojson as any);
239
+ let margin = [-0.5, -0.5, 0.5, 0.5];
240
+ for (let i = 0; i < 4; i++) {
241
+ bounds[i] += margin[i];
242
+ }
243
+
244
+ this.map.fitBounds(bounds as any);
245
+ }
246
+ }
247
+
248
+ private displayGeoJSON(
249
+ geojson: GeoJSON,
250
+ sourceName: string,
251
+ layerSpec: maplibregl.AddLayerObject,
252
+ moveView: boolean,
253
+ clear: boolean
254
+ ): Result<null, Error> {
255
+ if (geojson.features.length == 0) return ok(null);
57
256
 
58
257
  if (clear) {
59
258
  this.clear();
259
+ this.map.addSource(sourceName, {
260
+ type: 'geojson',
261
+ data: geojson as any,
262
+ });
263
+
264
+ this.map.addLayer(layerSpec);
265
+ } else if (!clear && !this.map.getSource(sourceName)) {
266
+ this.map.addSource(sourceName, {
267
+ type: 'geojson',
268
+ data: geojson as any,
269
+ });
270
+ this.map.addLayer(layerSpec);
271
+ } else {
272
+ this.updateSource(sourceName, geojson);
273
+ }
274
+ if (moveView) {
275
+ this.centerView(geojson);
276
+ }
277
+
278
+ this._sources.push(sourceName);
279
+ this._layers.push(layerSpec.id);
280
+
281
+ return ok(null);
282
+ }
283
+
284
+ private addStation(coord: number[]) {
285
+ const geometry = new THREE.ConeGeometry(50, 100, 3);
286
+ const material = new THREE.MeshPhongMaterial({
287
+ color: 0x0000ff,
288
+ shininess: 100,
289
+ side: THREE.DoubleSide,
290
+ });
291
+ let cone = new THREE.Mesh(geometry, material);
292
+
293
+ cone = cone.rotateX(THREE.MathUtils.degToRad(90));
294
+ cone.scale.set(50, 50, 50);
295
+ cone.position.y = 50;
296
+
297
+ const scale = MTP.SceneTransform.projectedUnitsPerMeter(48.58);
298
+ const rtcGroup = MTP.Creator.createRTCGroup(
299
+ coord,
300
+ [0, 0, 0],
301
+ [scale, scale, scale]
302
+ );
303
+
304
+ rtcGroup.add(cone);
305
+ this._mapscene.addObject(rtcGroup);
306
+ }
307
+
308
+ getEventNumber(): Result<number, Error> {
309
+ let source = this.getSourceGeoJSON();
310
+ let length = 0;
311
+ if (source.isErr()) return err(source.error);
312
+ source.map((s: EventGeoJSON) => {
313
+ length = s.features.length;
314
+ });
315
+
316
+ return ok(length);
317
+ }
318
+
319
+ getSourceGeoJSON(): Result<EventGeoJSON, Error> {
320
+ let source: maplibregl.GeoJSONSource | undefined =
321
+ this.map.getSource('src');
322
+ let data = source?._data.geojson;
323
+ if (data === undefined) {
324
+ return err(Error('Source not defined yet'));
60
325
  }
61
- if (eventList.features.length == 0) return;
326
+ return ok(data as EventGeoJSON);
327
+ }
328
+ }
62
329
 
63
- this.map.addSource('src', { type: 'geojson', data: eventList as any });
64
- eventList.features.forEach((feature) => {
65
- let eventType: string;
330
+ /*
331
+ let eventType: string;
66
332
  let typeName: string;
67
333
  if (!isEventGeoJSONProperties(feature.properties)) {
68
334
  eventType = feature.properties.eventType;
@@ -73,36 +339,5 @@ export class SismoMap {
73
339
  }
74
340
  let layerName = eventType == null ? 'event' : eventType;
75
341
  if (this.map.getLayer(layerName)) return;
76
- this.map.addLayer({
77
- id: layerName,
78
- source: 'src',
79
- type: 'circle',
80
- paint: {
81
- // Inspired by https://renass.unistra.fr/js/events.js
82
- 'circle-radius': ['*', 8, ['ln', ['+', 1, ['get', 'mag']]]],
83
- 'circle-color': '#B42222',
84
- 'circle-opacity': 0.8,
85
- 'circle-stroke-width': 1,
86
- 'circle-stroke-opacity': 0.9,
87
- },
88
- filter: ['==', eventType, ['get', typeName]],
89
- });
90
- });
91
- if (moveView) {
92
- if (eventList.features.length == 1) {
93
- let lng: number = eventList.features[0].geometry.coordinates[0];
94
- let lat: number = eventList.features[0].geometry.coordinates[1];
95
- this.map.flyTo({ center: [lng, lat], zoom: 6 });
96
- } else {
97
- // Inter-library operability is annoying, this line will always work with the types i'll give it
98
- let bounds = bbox(eventList as any);
99
- let margin = [-0.5, -0.5, 0.5, 0.5];
100
- for (let i = 0; i < 4; i++) {
101
- bounds[i] += margin[i];
102
- }
103
-
104
- this.map.fitBounds(bounds as any);
105
- }
106
- }
107
- }
108
- }
342
+
343
+ */
package/src/types.ts CHANGED
@@ -235,3 +235,101 @@ export type Form = {
235
235
  groups: QuestionGroup[];
236
236
  questions: Question[];
237
237
  };
238
+
239
+ export type InfosPhase = {
240
+ time: Date;
241
+ automatic: boolean;
242
+ distance: number;
243
+ azimuth: number;
244
+ arrivalID: number;
245
+ channelCode: string;
246
+ locationCode: string;
247
+ networkCode: string;
248
+ originID: number;
249
+ phaseCode: string;
250
+ pickID: number;
251
+ stationCode: string;
252
+ timeResidual: Date;
253
+ };
254
+
255
+ export type EventPhases = InfosPhase[];
256
+
257
+ export type Station = {
258
+ properties: {
259
+ networkcode: string;
260
+ networkcolor: string;
261
+ stationcode: string;
262
+ };
263
+ geometry: {
264
+ coordinates: number[];
265
+ crs: {
266
+ properties: {
267
+ name: string;
268
+ };
269
+ type: string;
270
+ };
271
+ type: string;
272
+ };
273
+ };
274
+
275
+ export type StationPhases = Station & {
276
+ properties: {
277
+ phases: EventPhases;
278
+ description: {
279
+ en: string;
280
+ fr: string;
281
+ }
282
+ };
283
+ };
284
+
285
+ export type StationGeoJSON = {
286
+ type: string;
287
+ features: StationPhases[];
288
+ };
289
+
290
+ export const ZoneNames = [
291
+ 'la-reunion',
292
+ 'france',
293
+ 'mayotte',
294
+ 'monde',
295
+ 'les-antilles',
296
+ ];
297
+
298
+ export type Zones = {
299
+ type: string;
300
+ features: Zone[];
301
+ };
302
+
303
+ export type Zone = {
304
+ properties: {
305
+ name: string;
306
+ buffer: number;
307
+ minimal_longitude: number;
308
+ maximal_longitude: number;
309
+ minimal_latitude: number;
310
+ maximal_latitude: number;
311
+ automatic_maximal_depth: number;
312
+ automatic_minimal_magnitude: number;
313
+ automatic_minimal_used_phase_count: number;
314
+ city_form_id: number;
315
+ individual_form_id: number;
316
+ manual_event_types: string[];
317
+ manual_maximal_depth: number;
318
+ manual_minimal_magnitude: number;
319
+ manual_minimal_used_phase_count: number;
320
+ show_automatic: boolean;
321
+ show_manual: boolean;
322
+ };
323
+ geometry: {
324
+ coordinates: number[][][][];
325
+ crs: {
326
+ properties: {
327
+ name: string;
328
+ };
329
+ type: string;
330
+ };
331
+ type: string;
332
+ };
333
+ };
334
+
335
+ export type GeoJSON = EventGeoJSON | StationGeoJSON;