diff --git a/client/public/databases/airbases/sinaimap.json b/client/public/databases/airbases/sinaimap.json index 25850239..c1f87765 100644 --- a/client/public/databases/airbases/sinaimap.json +++ b/client/public/databases/airbases/sinaimap.json @@ -1366,7 +1366,7 @@ "length": "9536" } ], - "TACAN": "", + "TACAN": "0X", "ICAO": "HECW", "elevation": "439" } diff --git a/client/src/schemas/airbases.schema.json b/client/src/schemas/airbases.schema.json index 2a47a001..c43f57d3 100644 --- a/client/src/schemas/airbases.schema.json +++ b/client/src/schemas/airbases.schema.json @@ -36,7 +36,7 @@ }, "magHeading": { "type": "string", - "pattern": "^([0-2][0-9]{2})|(3[0-6][0-9])?$" + "pattern": "^([0-2][0-9]{2})|(3(([0-5][0-9])|(60)))?$" } }, "required": ["magHeading"] diff --git a/client/src/schemas/importdata.schema.json b/client/src/schemas/importdata.schema.json new file mode 100644 index 00000000..7790f3b2 --- /dev/null +++ b/client/src/schemas/importdata.schema.json @@ -0,0 +1,377 @@ +{ + "$defs": { + "coalitionName": { + "enum": [ + "blue", + "neutral", + "red" + ], + "type": "string" + }, + "lat": { + "maximum": 90, + "minimum": -90, + "type": "number" + }, + "lng": { + "maximum": 180, + "minimum": -180, + "type": "number" + }, + "vec2": { + "additionalProperties": false, + "properties": { + "lat": { + "$ref": "#/$defs/lat" + }, + "lng": { + "$ref": "#/$defs/lng" + } + }, + "required": [ + "lat", + "lng" + ], + "type": "object" + }, + "vec3": { + "additionalProperties": false, + "properties": { + "alt": { + "type": "number" + }, + "lat": { + "$ref": "#/$defs/lat" + }, + "lng": { + "$ref": "#/$defs/lng" + } + }, + "required": [ + "alt", + "lat", + "lng" + ], + "type": "object" + } + }, + "patternProperties": { + ".*": { + "items": { + "additionalProperties": false, + "properties": { + "activePath": { + "items": { + "$ref": "#/$defs/vec3" + }, + "type": "array" + }, + "alive": { + "type": "boolean" + }, + "ammo": { + "items": { + "additionalProperties": false, + "properties": { + "category": { + "minimum": 0, + "type": "number" + }, + "guidance": { + "minimum": 0, + "type": "number" + }, + "missileCategory": { + "minimum": 0, + "type": "number" + }, + "name": { + "minLength": 3, + "type": "string" + }, + "quantity": { + "minimum": 0, + "type": "number" + } + }, + "required": [ + "quantity", + "name", + "guidance", + "category", + "missileCategory" + ], + "type": "object" + }, + "type": "array" + }, + "category": { + "type": "string" + }, + "categoryDisplayName": { + "type": "string" + }, + "coalition": { + "$ref": "#/$defs/coalitionName" + }, + "contacts": { + "type": "array" + }, + "controlled": { + "type": "boolean" + }, + "country": { + "type": "number" + }, + "desiredAltitude": { + "minimum": 0, + "type": "number" + }, + "desiredAltitudeType": { + "enum": [ + "AGL", + "ASL" + ], + "type": "string" + }, + "desiredSpeed": { + "minimum": 0, + "type": "number" + }, + "desiredSpeedType": { + "enum": [ + "CAS", + "GS" + ], + "type": "string" + }, + "emissionsCountermeasures": { + "enum": [ + "attac", + "defend", + "free", + "silent" + ], + "type": "string" + }, + "followRoads": { + "type": "boolean" + }, + "formationOffset": { + "additionalProperties": false, + "properties": { + "x": { + "minimum": 0, + "type": "number" + }, + "y": { + "minimum": 0, + "type": "number" + }, + "z": { + "minimum": 0, + "type": "number" + } + }, + "required": [ + "x", + "y", + "z" + ], + "type": "object" + }, + "fuel": { + "maximum": 100, + "minimum": 0, + "type": "number" + }, + "generalSettings": { + "additionalProperties": false, + "properties": { + "prohibitAA": { + "type": "boolean" + }, + "prohibitAfterburner": { + "type": "boolean" + }, + "prohibitAG": { + "type": "boolean" + }, + "prohibitAirWpn": { + "type": "boolean" + }, + "prohibitJettison": { + "type": "boolean" + } + }, + "required": [ + "prohibitAA", + "prohibitAfterburner", + "prohibitAG", + "prohibitAirWpn", + "prohibitJettison" + ], + "type": "object" + }, + "groupName": { + "type": "string" + }, + "hasTask": { + "type": "boolean" + }, + "heading": { + "type": "number" + }, + "health": { + "maximum": 100, + "minimum": 0, + "type": "number" + }, + "horizontalVelocity": { + "minimum": 0, + "type": "number" + }, + "human": { + "type": "boolean" + }, + "ID": { + "type": "number" + }, + "isActiveAWACS": { + "type": "boolean" + }, + "isActiveTanker": { + "type": "boolean" + }, + "isLeader": { + "type": "boolean" + }, + "leaderID": { + "minimum": 0, + "type": "number" + }, + "name": { + "type": "string" + }, + "onOff": { + "type": "boolean" + }, + "operateAs": { + "$ref": "#/$defs/coalitionName" + }, + "position": { + "$ref": "#/$defs/vec3" + }, + "radio": { + "additionalProperties": false, + "properties": { + "callsign": { + "type": "number" + }, + "callsignNumber": { + "type": "number" + }, + "frequency": { + "type": "number" + } + }, + "required": [ + "frequency", + "callsign", + "callsignNumber" + ], + "type": "object" + }, + "reactionToThreat": { + "enum": [ + "evade", + "maneouvre", + "none", + "passive" + ], + "type": "string" + }, + "ROE": { + "enum": [ + "designated", + "free", + "hold", + "return" + ], + "type": "string" + }, + "shotsIntensity": { + "maximum": 3, + "minimum": 1, + "type": "number" + }, + "shotsScatter": { + "maximum": 3, + "minimum": 1, + "type": "number" + }, + "speed": { + "minimum": 0, + "type": "number" + }, + "state": { + "type": "string" + }, + "TACAN": { + "properties": { + "callsign": { + "type": "string" + }, + "channel": { + "minimum": 0, + "type": "number" + }, + "isOn": { + "type": "boolean" + }, + "XY": { + "enum": [ + "X", + "Y" + ], + "type": "string" + } + }, + "required": [ + "callsign", + "channel", + "isOn", + "XY" + ], + "type": "object" + }, + "targetID": { + "minimum": 0, + "type": "number" + }, + "targetPosition": { + "$ref": "#/$defs/vec2" + }, + "task": { + "type": "string" + }, + "track": { + "type": "number" + }, + "unitName": { + "type": "string" + }, + "verticalVelocity": { + "minimum": 0, + "type": "number" + } + }, + "type": "object", + "required": ["activePath","alive","ammo","category","categoryDisplayName","coalition","contacts","controlled","country","desiredAltitude","desiredAltitudeType","desiredSpeed","desiredSpeedType","emissionsCountermeasures","followRoads","formationOffset","fuel","generalSettings","groupName","hasTask","heading","health","horizontalVelocity","human","ID","isActiveAWACS","isActiveTanker","isLeader","leaderID","name","onOff","operateAs","position","radio","reactionToThreat","ROE","shotsIntensity","shotsScatter","speed","state","TACAN","targetID","targetPosition","task","track","unitName","verticalVelocity"] + }, + "minItems": 1, + "type": "array" + } + }, + "type": "object" +} \ No newline at end of file diff --git a/client/src/schemas/schema.ts b/client/src/schemas/schema.ts index 8f4a33b6..5ad4c527 100644 --- a/client/src/schemas/schema.ts +++ b/client/src/schemas/schema.ts @@ -1,34 +1,55 @@ -import Ajv, { JSONSchemaType } from "ajv"; -import { AnySchemaObject, Schema } from "ajv/dist/core"; +import Ajv from "ajv"; +import { AnySchemaObject } from "ajv/dist/core"; // For future extension abstract class JSONSchemaValidator { + #ajv:Ajv; + #compiledValidator:any; #schema!:AnySchemaObject; constructor( schema:AnySchemaObject ) { this.#schema = schema; + + this.#ajv = new Ajv({ + "allErrors": true + }); + + this.#compiledValidator = this.getAjv().compile(this.getSchema()); + + } + + getAjv() { + return this.#ajv; + } + + getCompiledValidator() { + return this.#compiledValidator; + } + + getErrors() { + return this.getCompiledValidator().errors; } getSchema() { return this.#schema; } + validate(data:any) { + return (this.getCompiledValidator())(data); + } + } export class AirbasesJSONSchemaValidator extends JSONSchemaValidator { - constructor( ) { + constructor() { const schema = require("../schemas/airbases.schema.json"); super( schema ); - - const ajv = new Ajv({ - "allErrors": true - }); [ require( "../../public/databases/airbases/caucasus.json" ), @@ -41,11 +62,21 @@ export class AirbasesJSONSchemaValidator extends JSONSchemaValidator { require( "../../public/databases/airbases/syria.json" ), require( "../../public/databases/airbases/thechannel.json" ) ].forEach( data => { - const validate = ajv.compile(this.getSchema()); + const validate = this.getAjv().compile(this.getSchema()); const valid = validate(data); if (!valid) console.error(validate.errors); }); } +} + + +export class ImportFileJSONSchemaValidator extends JSONSchemaValidator { + + constructor() { + const schema = require("../schemas/importdata.schema.json"); + super( schema ); + } + } \ No newline at end of file diff --git a/client/src/unit/importexport/unitdatafileexport.ts b/client/src/unit/importexport/unitdatafileexport.ts index 1bdb0d22..2a4f7a82 100644 --- a/client/src/unit/importexport/unitdatafileexport.ts +++ b/client/src/unit/importexport/unitdatafileexport.ts @@ -25,6 +25,10 @@ export class UnitDataFileExport extends UnitDataFile { * Show the form to start the export journey */ showForm(units: Unit[]) { + this.dialog.getElement().querySelectorAll("[data-on-error]").forEach((el:Element) => { + el.classList.toggle("hide", el.getAttribute("data-on-error") === "show"); + }); + const data: any = {}; const unitCanBeExported = (unit: Unit) => !["Aircraft", "Helicopter"].includes(unit.getCategory()); diff --git a/client/src/unit/importexport/unitdatafileimport.ts b/client/src/unit/importexport/unitdatafileimport.ts index 5a0e2221..9907f6f2 100644 --- a/client/src/unit/importexport/unitdatafileimport.ts +++ b/client/src/unit/importexport/unitdatafileimport.ts @@ -1,6 +1,7 @@ import { getApp } from "../.."; import { Dialog } from "../../dialog/dialog"; import { UnitData } from "../../interfaces"; +import { ImportFileJSONSchemaValidator } from "../../schemas/schema"; import { UnitDataFile } from "./unitdatafile"; export class UnitDataFileImport extends UnitDataFile { @@ -48,21 +49,59 @@ export class UnitDataFileImport extends UnitDataFile { unitsManager.spawnUnits(category, unitsToSpawn, coalition, false); } + } - /* - for (let groupName in groups) { - if (groupName !== "" && groups[groupName].length > 0 && (groups[groupName].every((unit: UnitData) => { return unit.category == "GroundUnit"; }) || groups[groupName].every((unit: any) => { return unit.category == "NavyUnit"; }))) { - var aliveUnits = groups[groupName].filter((unit: UnitData) => { return unit.alive }); - var units = aliveUnits.map((unit: UnitData) => { - return { unitType: unit.name, location: unit.position, liveryID: "" } - }); - getApp().getUnitsManager().spawnUnits(groups[groupName][0].category, units, groups[groupName][0].coalition, true); + selectFile() { + var input = document.createElement("input"); + input.type = "file"; + input.addEventListener("change", (e: any) => { + var file = e.target.files[0]; + if (!file) { + return; } - } - //*/ + var reader = new FileReader(); + reader.onload = (e: any) => { + + try { + this.#fileData = JSON.parse(e.target.result); + + const validator = new ImportFileJSONSchemaValidator(); + if (!validator.validate(this.#fileData)) { + const errors = validator.getErrors().reduce((acc:any, error:any) => { + acc.push(error.instancePath + ": " + error.message) + return acc; + }, [] as string[]); + this.#showFileDataErrors(errors); + } else { + this.#showForm(); + } + } catch(e:any) { + this.#showFileDataErrors([e]); + } + }; + reader.readAsText(file); + }) + input.click(); + } + + #showFileDataErrors( reasons:string[]) { + + this.dialog.getElement().querySelectorAll("[data-on-error]").forEach((el:Element) => { + el.classList.toggle("hide", el.getAttribute("data-on-error") === "hide"); + }); + + const reasonsList = this.dialog.getElement().querySelector(".import-error-reasons"); + if (reasonsList instanceof HTMLElement) + reasonsList.innerHTML = `
<%= textContent %>
- <% if (showFilenameInput) { %> -<%= textContent %>
+ + <% if (showFilenameInput) { %> +