Notes
Notes - notes.io |
import { FormGroup, FormArray, FormControl, FormBuilder, AbstractControl, Validators } from '@angular/forms';
import { ThemeService } from 'ng2-charts';
import { Subject, takeUntil } from 'rxjs';
import { EnvironmentService, CesiumService, LocationService, ApisService, StatusBarService, GlobalVariablesService } from 'src/app/services';
import { AirPlanningService } from 'src/app/services/air-planning.service';
@Component({
selector: 'app-air-planning-popup',
templateUrl: './air-planning-popup.component.html',
styleUrls: ['./air-planning-popup.component.css']
})
export class AirPlanningPopupComponent implements OnInit {
@Input() form!: FormGroup;
@Input() data: any;
@Output() addPlanningEntry = new EventEmitter<any>();
startZuluTime: string = '';
endZuluTime: string = '';
ngUnsubscribe: Subject<any>;
isStartPointFocused = false;
startPointMGRS: string = "";
startPointAltitude: number = 0;
startPointEntity: any;
isSingleWaypointFocused = false;
wayPointIndex: number = 0;
wayPointMGRS: string = "";
wayPointAltitude: number = 0;
waypointEntities: any[] = []; // Track each waypoint's entity
ShowAllPaths = false;
singleWaypointEntity: any = null;
previewPathData: {
property: any,
arcStartTime?: any,
arcDuration?: number,
totalDuration: number,
positions: any,
indexCount: number,
startTime?: any,
labelPoints?: { time: any, label: string, speed?: number }[],
bearingProperty?: any,
speed: any,
entityIds?: string[]
} | null = null;
aircraftModalOptions = [
{ label: 'A-10', value: '3341638' },
{ label: 'F-35', value: '3414257' },
];
constructor(
private fb: FormBuilder,
private environmentService: EnvironmentService,
private cesiumService: CesiumService,
public locationService: LocationService,
private globalVariable: GlobalVariablesService,
private globalVariablesService: GlobalVariablesService,
private planningDataService: AirPlanningService
) {
this.ngUnsubscribe = new Subject();
}
ngOnInit(): void {
if (this.data) {
this.startZuluTime = this.data.startZuluTime || '';
this.endZuluTime = this.data.endZuluTime || '';
}
Cesium.Ion.defaultAccessToken = this.environmentService.getEnvironment().cesiumToken;
this.initializeProperties();
this.handleAllSubscriptions();
}
initializeProperties(): void {
this.form = this.fb.group({
originalName: [''],
aircraftType: ['3341638', Validators.required],
startPoint: ['', Validators.required],
altitude: ['', Validators.required],
rangeMiles: [null], // <- NEW
bearingDegrees: [null, [Validators.min(0), Validators.max(12)]],
startZuluTime: [this.startZuluTime],
endZuluTime: [this.endZuluTime],
points: this.fb.array([])
});
}
handleAllSubscriptions() {
this.locationService.mgrsChangeEvent
.pipe(takeUntil(this.ngUnsubscribe))
.subscribe(async (mgrs: string) => {
// this.AddPlacemark()
if (this.isStartPointFocused) {
this.startPointMGRS = mgrs;
this.startPointAltitude = this.form.get("altitude").value;
if (this.startPointEntity) {
this.cesiumService.viewer.entities.remove(this.startPointEntity);
}
this.startPointEntity = await this.addEntity(
this.locationService.MGRSToLL(this.startPointMGRS),
'Start',
this.startPointAltitude
);
this.form.patchValue({
startPoint: mgrs
});
this.cesiumService.viewer.scene.requestRender();
this.removeAllFocus();
}
else if (this.points.at(this.wayPointIndex)?.get('isWayPointFocused')?.value) {
const pointGroup = this.points.at(this.wayPointIndex) as FormGroup;
this.wayPointMGRS = mgrs;
this.wayPointAltitude = pointGroup.get("altitude")?.value || 0;
this.waypointEntities[this.wayPointIndex] = await this.addEntity(
this.locationService.MGRSToLL(this.wayPointMGRS),
(this.wayPointIndex + 1).toString(),
this.wayPointAltitude
);
// Update only the selected FormGroup's MGRS
pointGroup.patchValue({ mgrs: mgrs });
this.cesiumService.viewer.scene.requestRender();
this.removeAllFocus();
}
});
this.form.valueChanges.pipe(takeUntil(this.ngUnsubscribe)).subscribe(() => {
if (this.form.valid) {
this.removeEntities();
this.previewFlightPath();
}
});
}
get points(): FormArray {
let data = this.form.get('points') as FormArray;
return data;
}
async previewFlightPath(): Promise<void> {
// ✅ Remove previously drawn entities for this path only
if (this.previewPathData?.entityIds?.length) {
this.removeEntities();
}
const formData = this.form.value;
const entityIds: string[] = [];
const result = await this.buildFlightPath(formData);
if (!result) return;
const { property, labelPoints } = result;
const yellowPositions: any[] = [];
const times = property._property._times;
for (const time of times) {
const pos = property.getValue(time);
if (pos) yellowPositions.push(pos);
}
// 🟡 Draw yellow path
const yellowPathEntity = this.cesiumService.viewer.entities.add({
polyline: {
positions: yellowPositions,
width: 2,
material: Cesium.Color.YELLOW
}
});
entityIds.push(yellowPathEntity.id);
// 🟠 Optional bearing path
const bearingDeg = +formData.bearingDegrees;
const rangeMiles = +formData.rangeMiles;
if (bearingDeg && rangeMiles) {
const bearingProperty = new Cesium.SampledPositionProperty();
const orangePositions: any[] = [];
for (const time of times) {
const pos = property.getValue(time);
if (!pos) continue;
const carto = Cesium.Cartographic.fromCartesian(pos);
const lat = Cesium.Math.toDegrees(carto.latitude);
const lon = Cesium.Math.toDegrees(carto.longitude);
const alt = carto.height;
const offset = this.calculateOffsetByHeadingAngle(lat, lon, bearingDeg, rangeMiles);
const offsetCartesian = Cesium.Cartesian3.fromDegrees(offset.lon, offset.lat, alt);
bearingProperty.addSample(time, offsetCartesian);
orangePositions.push(offsetCartesian);
}
const orangePathEntity = this.cesiumService.viewer.entities.add({
polyline: {
positions: orangePositions,
width: 2,
material: Cesium.Color.ORANGE
}
});
entityIds.push(orangePathEntity.id);
const usedLabels = new Set<string>();
for (const lbl of labelPoints) {
const time = lbl.time;
const label = lbl.label;
const pos = bearingProperty.getValue(time);
if (pos && !usedLabels.has(label)) {
usedLabels.add(label);
const labelEntity = this.addPointToGlobe(pos, label, lbl.speed, Cesium.Color.ORANGE);
if (labelEntity?.id) entityIds.push(labelEntity.id);
}
}
this.previewPathData.bearingProperty = bearingProperty;
}
this.previewPathData = { ...result, entityIds };
}
private async buildFlightPath(formData: any): Promise<{
property: any;
arcStartTime: any;
arcDuration: number;
totalDuration: number;
positions: any;
indexCount: number;
startTime: any;
labelPoints: { time: any, label: string, speed?: number }[];
speed: any;
}> {
this.cesiumService.viewer.scene.globe.depthTestAgainstTerrain = false;
let startTime: any, endTime: any;
try {
const timeStr = formData.startZuluTime;
const now = new Date();
const zuluTimeStr = `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, '0')}-${String(now.getUTCDate()).padStart(2, '0')}T${timeStr}:00Z`;
startTime = Cesium.JulianDate.fromDate(new Date(zuluTimeStr));
} catch {
alert("Invalid Zulu start time format.");
return;
}
try {
const timeStr = formData.endZuluTime;
const now = new Date();
const zuluTimeStr = `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, '0')}-${String(now.getUTCDate()).padStart(2, '0')}T${timeStr}:00Z`;
endTime = Cesium.JulianDate.fromDate(new Date(zuluTimeStr));
} catch {
alert("Invalid Zulu end time format.");
return;
}
const totalDuration = Cesium.JulianDate.secondsDifference(endTime, startTime);
const property = new Cesium.SampledPositionProperty();
let indexCount = 0;
this.globalVariable.ShowTimeLine.next(true);
this.cesiumService.viewer.timeline.container.style.display = 'block';
this.cesiumService.viewer.animation.container.style.display = 'block';
this.cesiumService.viewer.scene.globe.enableLighting = true;
const arcSegments = 180;
const labelPoints: { time: any, label: string, speed?: number }[] = [];
const [validStart, startLat, startLon] = this.locationService.MGRSToLL(formData.startPoint) as [boolean, number, number];
if (!validStart || isNaN(startLat) || isNaN(startLon)) {
return;
}
const startAltitude = +formData.altitude * 0.3048;
let positions: any = {};
positions.startPoint = [startAltitude, startLon, startLat];
positions.endPoint = positions.startPoint;
const startPosition = Cesium.Cartesian3.fromDegrees(startLon, startLat, startAltitude);
this.addPointToGlobe(startPosition, 'Start', formData.speed, Cesium.Color.BLUE);
property.addSample(startTime, startPosition);
labelPoints.push({ time: startTime, label: 'Start', speed: +formData.speed });
const fullPointList = [...formData.points];
const segments: any[] = [];
let totalDistance = 0;
let segmentTimeOffset = 0;
for (let i = 0; i < fullPointList.length; i++) {
const entry = fullPointList[i];
if (entry.type === 'waypoint' && entry.mgrs) {
const [valid, lat, lon] = this.locationService.MGRSToLL(entry.mgrs) as [boolean, number, number];
if (!valid || isNaN(lat) || isNaN(lon)) return;
const altitude = +entry.altitude * 0.3048;
const startCarto = Cesium.Cartographic.fromDegrees(positions.endPoint[1], positions.endPoint[2], positions.endPoint[0]);
const endCarto = Cesium.Cartographic.fromDegrees(lon, lat, altitude);
const startPos = Cesium.Cartesian3.fromDegrees(positions.endPoint[1], positions.endPoint[2], positions.endPoint[0]);
const endPos = Cesium.Cartesian3.fromDegrees(lon, lat, altitude);
const dist = Cesium.Cartesian3.distance(startPos, endPos);
segments.push({
type: 'waypoint',
startCarto,
endCarto,
startAlt: positions.endPoint[0],
endAlt: altitude,
dist,
label: `WP${i + 1}`,
speed: +entry.speed
});
totalDistance += dist;
this.addPointToGlobe(endPos, `WP${i + 1}`, +entry.speed, Cesium.Color.BLUE);
positions.startPoint = positions.endPoint;
positions.endPoint = [altitude, lon, lat];
} else if (entry.type === 'turnrate' && entry.turnRate) {
const turnRateDegPerSec = +entry.turnRate;
const altitude = +entry.altitude * 0.3048;
const pointA = positions.startPoint;
const pointB = positions.endPoint;
const headingAB = this.computeHeading(pointA, pointB);
const isLeftTurn = true;
const turnDirectionAngle = isLeftTurn ? Math.PI / 2 : -Math.PI / 2;
const perpHeading = headingAB + turnDirectionAngle;
const cartoB = Cesium.Cartographic.fromDegrees(pointB[1], pointB[2], pointB[0]);
const cosLat = Math.cos(cartoB.latitude);
const earthRadius = Cesium.Ellipsoid.WGS84.maximumRadius;
const fallbackSpeedKts = 250;
const fallbackSpeedMps = fallbackSpeedKts * 0.514444;
const turnRadius = fallbackSpeedMps / Cesium.Math.toRadians(turnRateDegPerSec);
const offsetLat = (turnRadius / earthRadius) * Math.cos(perpHeading);
const offsetLon = (turnRadius / (earthRadius * cosLat)) * Math.sin(perpHeading);
const center = new Cesium.Cartographic(
cartoB.longitude + offsetLon,
cartoB.latitude + offsetLat,
altitude
);
const radiusRad = turnRadius / earthRadius;
const startAngle = Math.atan2(
cartoB.latitude - center.latitude,
(cartoB.longitude - center.longitude) * Math.cos(center.latitude)
);
const sweep = isLeftTurn ? -Math.PI : Math.PI;
const arcPoints: any[] = [];
for (let j = 0; j <= arcSegments; j++) {
const angle = startAngle + (sweep * j / arcSegments);
const lat = center.latitude + (radiusRad * Math.sin(angle));
const lon = center.longitude + (radiusRad * Math.cos(angle)) / Math.cos(center.latitude);
const cartesian = Cesium.Cartesian3.fromRadians(lon, lat, altitude);
arcPoints.push(cartesian);
}
let arcLength = 0;
for (let j = 1; j < arcPoints.length; j++) {
arcLength += Cesium.Cartesian3.distance(arcPoints[j - 1], arcPoints[j]);
}
const arcEndPoint = arcPoints[arcPoints.length - 1];
segments.push({
type: 'turnrate',
arcPoints,
arcLength,
label: `TR${i + 1}`,
endPoint: arcEndPoint,
altitude,
speed: undefined
});
totalDistance += arcLength;
// ✅ Update positions.endPoint after turnrate
const arcEndCarto = Cesium.Cartographic.fromCartesian(arcEndPoint);
positions.endPoint = [
arcEndCarto.height,
Cesium.Math.toDegrees(arcEndCarto.longitude),
Cesium.Math.toDegrees(arcEndCarto.latitude)
];
}
}
// Compute speed
const totalDistanceMiles = totalDistance / 1609.34;
const totalDurationMinutes = totalDuration / 60;
const speed = totalDistanceMiles / totalDurationMinutes;
for (const seg of segments) {
if (seg.type === 'waypoint') {
const geodesic = new Cesium.EllipsoidGeodesic(seg.startCarto, seg.endCarto);
const duration = (seg.dist / totalDistance) * totalDuration;
const samples = 50;
for (let j = 0; j <= samples; j++) {
const f = j / samples;
const interpCarto = geodesic.interpolateUsingFraction(f);
const interpAlt = Cesium.Math.lerp(seg.startAlt, seg.endAlt, f);
const cartesian = Cesium.Cartesian3.fromRadians(interpCarto.longitude, interpCarto.latitude, interpAlt);
const t = Cesium.JulianDate.addSeconds(startTime, segmentTimeOffset + (duration * f), new Cesium.JulianDate());
property.addSample(t, cartesian);
}
segmentTimeOffset += duration;
const t = Cesium.JulianDate.addSeconds(startTime, segmentTimeOffset, new Cesium.JulianDate());
labelPoints.push({ time: t, label: seg.label, speed: seg.speed });
} else if (seg.type === 'turnrate') {
const duration = (seg.arcLength / totalDistance) * totalDuration;
for (let j = 0; j <= arcSegments; j++) {
const f = j / arcSegments;
const t = Cesium.JulianDate.addSeconds(startTime, segmentTimeOffset + (duration * f), new Cesium.JulianDate());
property.addSample(t, seg.arcPoints[j]);
}
segmentTimeOffset += duration;
const t = Cesium.JulianDate.addSeconds(startTime, segmentTimeOffset, new Cesium.JulianDate());
labelPoints.push({ time: t, label: seg.label });
this.addPointToGlobe(seg.endPoint, seg.label, seg.speed, Cesium.Color.BLUE);
}
}
const intervalSeconds = 60;
let labelSeconds = intervalSeconds;
let nextIntervalTime = Cesium.JulianDate.addSeconds(startTime, intervalSeconds, new Cesium.JulianDate());
while (Cesium.JulianDate.lessThanOrEquals(nextIntervalTime, endTime)) {
const interpolatedPos = property.getValue(nextIntervalTime);
if (interpolatedPos) {
const label = Math.floor(labelSeconds / 60) + ' m';
this.addPointToGlobe(interpolatedPos, label, undefined, Cesium.Color.YELLOW);
labelPoints.push({ time: Cesium.JulianDate.clone(nextIntervalTime), label });
labelSeconds += intervalSeconds;
}
nextIntervalTime = Cesium.JulianDate.addSeconds(nextIntervalTime, intervalSeconds, new Cesium.JulianDate());
}
return {
property,
arcStartTime: null,
arcDuration: 0,
totalDuration,
positions,
indexCount,
startTime,
labelPoints,
speed
};
}
addPointToGlobe(position: any, label: string, speed?: number, pointColor = Cesium.Color.YELLOW): any {
const entity = this.cesiumService.viewer.entities.add({
position,
point: {
pixelSize: 10,
color: pointColor,
outlineColor: Cesium.Color.BLACK,
outlineWidth: 1
},
label: {
text: label,
font: '14px sans-serif',
fillColor: Cesium.Color.WHITE,
outlineColor: Cesium.Color.BLACK,
outlineWidth: 2,
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
verticalOrigin: Cesium.VerticalOrigin.TOP,
pixelOffset: new Cesium.Cartesian2(0, -18)
}
});
return entity; // ✅ return the entity
}
calculateOffsetByHeadingAngle(
lat: number,
lon: number,
clockBearing: number,
rangeMiles: number
): { lat: number; lon: number } {
const R = 6371e3; // Earth radius in meters
const distanceMeters = rangeMiles * 1609.34;
const φ1 = Cesium.Math.toRadians(lat);
const λ1 = Cesium.Math.toRadians(lon);
// Convert clock bearing (1-12) to degrees
const headingDeg = clockBearing * 30; // 12 becomes 0
const θ = Cesium.Math.toRadians(headingDeg);
const δ = distanceMeters / R;
const φ2 = Math.asin(
Math.sin(φ1) * Math.cos(δ) +
Math.cos(φ1) * Math.sin(δ) * Math.cos(θ)
);
const λ2 = λ1 + Math.atan2(
Math.sin(θ) * Math.sin(δ) * Math.cos(φ1),
Math.cos(δ) - Math.sin(φ1) * Math.sin(φ2)
);
return {
lat: Cesium.Math.toDegrees(φ2),
lon: Cesium.Math.toDegrees(λ2)
};
}
computeHeading(from: any, to: any): number {
const fromCarto = Cesium.Cartographic.fromDegrees(from[1], from[2]);
const toCarto = Cesium.Cartographic.fromDegrees(to[1], to[2]);
const dLon = toCarto.longitude - fromCarto.longitude;
const y = Math.sin(dLon) * Math.cos(toCarto.latitude);
const x =
Math.cos(fromCarto.latitude) * Math.sin(toCarto.latitude) -
Math.sin(fromCarto.latitude) * Math.cos(toCarto.latitude) * Math.cos(dLon);
return Math.atan2(y, x);
}
addEntity(latLong, text, altitude): Promise<any> {
return new Promise((resolve) => {
const icon = 'assets/images/DataManager/DefaultIcons/DefaultIconr.png';
const point = Cesium.Cartesian3.fromDegrees(latLong[2], latLong[1], altitude);
let entityObject: any = {
description: `Location: (${latLong[2]}, ${latLong[1]}, ${altitude})`,
position: point,
billboard: {
image: icon,
opacity: 100,
scale: 0.75,
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
position: point,
},
label: {
font: 'bold 16pt sans-serif',
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
pixelOffset: new Cesium.Cartesian2(0, -50),
outlineColor: Cesium.Color.BLACK,
outlineWidth: 5,
scale: 1.0,
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
fillColor: Cesium.Color.fromCssColorString('#fff'),
eyeOffset: new Cesium.Cartesian3(0, 100, 0),
show: true,
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
},
};
let canvas: HTMLCanvasElement = document.createElement('canvas');
let context = canvas.getContext('2d');
let img1 = new Image();
img1.src = `assets/images/DataManager/DefaultIcons/DefaultIcony.png`;
img1.onload = () => {
canvas.width = 40;
canvas.height = 60;
context.drawImage(img1, 0, 0, 40, 60);
context.fillStyle = 'black';
context.font = 'bold 20pt Calibri';
const isStartOrEnd = text === 'Start' || text === 'End';
const shortText = isStartOrEnd ? text[0] : text;
const offsetX = !isStartOrEnd && text.length > 1 ? 6 : 12;
context.fillText(shortText, offsetX, 25);
entityObject.billboard.image = canvas.toDataURL('image/png');
entityObject.billboard.width = 40;
entityObject.billboard.height = 60;
entityObject.heightReference = Cesium.HeightReference.CLAMP_TO_GROUND;
const entity = this.cesiumService.viewer.entities.add(entityObject);
resolve(entity); // return the added entity
};
});
}
removeAllFocus(): void {
this.isStartPointFocused = false;
this.isSingleWaypointFocused = false;
this.points.controls.forEach((w: AbstractControl) => {
w.get('isWayPointFocused')?.setValue(false);
});
}
// Add a new dynamic point (waypoint or turn-rate)
addEntry(): void {
const pointGroup = this.fb.group({
type: ['waypoint', Validators.required],
mgrs: [''],
turnRate: [''],
altitude: [this.form.get('altitude')?.value, Validators.required],
isWayPointFocused: [false]
});
this.points.push(pointGroup);
}
removeEntry(index: number): void {
this.points.removeAt(index);
}
onCancelClick(): void {
this.locationService.mgrs = '';
this.form.get('startPoint').reset();
if (this.startPointEntity) {
this.cesiumService.viewer.entities.remove(this.startPointEntity);
this.startPointEntity = null;
}
this.changeCursorToSelectLocation();
this.isStartPointFocused = true;
this.cesiumService.viewer.scene.requestRender();
}
onWaypointCancelClick(index: number): void {
const pointGroup = this.points.at(index) as FormGroup;
if (this.waypointEntities[index]) {
this.cesiumService.viewer.entities.remove(this.waypointEntities[index]);
this.waypointEntities[index] = null;
}
pointGroup.patchValue({
mgrs: '',
altitude: '',
speed: ''
});
pointGroup.get('isWayPointFocused')?.setValue(true);
this.wayPointIndex = index;
this.changeCursorToSelectLocation();
this.cesiumService.viewer.scene.requestRender();
}
pointFocus(p): void {
this.removeAllFocus();
this.changeCursorToSelectLocation();
switch (p) {
case 'start':
this.isStartPointFocused = true;
break;
case 'waypoint':
this.isSingleWaypointFocused = true;
break;
default:
this.removeAllFocus();
this.wayPointIndex = p;
this.points.at(p)?.get('isWayPointFocused')?.setValue(true);
break;
}
}
changeCursorToSelectLocation(): void {
this.cesiumService.viewer._container.style.cursor = "url('assets/images/V02/crosshair_cursor.png') 32 32, auto";
}
changeCursorToDefault(): void {
this.cesiumService.viewer._container.style.cursor = 'default';
}
AddPath(): void {
this.globalVariablesService.closePopupEvent.emit('AirPlanningType');
if (this.form.valid) {
this.planningDataService.emitPlanningEntry(this.form.value);
this.form.reset();
this.points.clear();
}
}
removeEntities() {
if (this.startPointEntity) {
this.cesiumService.viewer.entities.remove(this.startPointEntity);
this.startPointEntity = null;
}
for (const entity of this.waypointEntities) {
this.cesiumService.viewer.entities.remove(entity);
}
this.waypointEntities = [];
const viewer = this.cesiumService.viewer;
this.previewPathData?.entityIds?.forEach(id => {
const entity = viewer.entities.getById(id);
if (entity) {
viewer.entities.remove(entity);
}
});
this.cesiumService.viewer.scene.requestRender();
}
ngOnDestroy(): void {
debugger;
this.form.reset();
this.points.clear();
this.removeEntities();
this.cesiumService.viewer.scene.requestRender();
this.previewPathData = null;
this.ngUnsubscribe.complete();
}
}
![]() |
Notes is a web-based application for online taking notes. You can take your notes and share with others people. If you like taking long notes, notes.io is designed for you. To date, over 8,000,000,000+ notes created and continuing...
With notes.io;
- * You can take a note from anywhere and any device with internet connection.
- * You can share the notes in social platforms (YouTube, Facebook, Twitter, instagram etc.).
- * You can quickly share your contents without website, blog and e-mail.
- * You don't need to create any Account to share a note. As you wish you can use quick, easy and best shortened notes with sms, websites, e-mail, or messaging services (WhatsApp, iMessage, Telegram, Signal).
- * Notes.io has fabulous infrastructure design for a short link and allows you to share the note as an easy and understandable link.
Fast: Notes.io is built for speed and performance. You can take a notes quickly and browse your archive.
Easy: Notes.io doesn’t require installation. Just write and share note!
Short: Notes.io’s url just 8 character. You’ll get shorten link of your note when you want to share. (Ex: notes.io/q )
Free: Notes.io works for 14 years and has been free since the day it was started.
You immediately create your first note and start sharing with the ones you wish. If you want to contact us, you can use the following communication channels;
Email: [email protected]
Twitter: http://twitter.com/notesio
Instagram: http://instagram.com/notes.io
Facebook: http://facebook.com/notesio
Regards;
Notes.io Team
