Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add kml selection exports #5300

Draft
wants to merge 5 commits into
base: production
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 164 additions & 8 deletions specifyweb/frontend/js_src/lib/components/QueryBuilder/Export.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ import { QueryButton } from './Components';
import type { QueryField } from './helpers';
import { hasLocalityColumns } from './helpers';
import type { QueryResultRow } from './Results';
import { format } from '../Formatters/formatters';
import { jsonToXml, XmlNode, SimpleXmlNode } from '../Syncer/xmlToJson';
import { downloadFile } from '../Molecules/FilePicker';

export function QueryExportButtons({
baseTableName,
Expand Down Expand Up @@ -89,16 +92,23 @@ export function QueryExportButtons({
'exportCsvUtf8Bom'
);

function formatExportFileName(
file_extension: string
): string {
return `${
queryResource.isNew()
? `${queryText.newQueryName()} ${genericTables[baseTableName].label}`
: queryResource.get('name')
} - ${new Date().toDateString()}.${file_extension}`;
}

/*
*Will be only called if query is not distinct,
*selection not enabled when distinct selected
*/
async function exportSelected(): Promise<void> {
const name = `${
queryResource.isNew()
? `${queryText.newQueryName()} ${genericTables[baseTableName].label}`
: queryResource.get('name')
} - ${new Date().toDateString()}.csv`;

async function exportCsvSelected(): Promise<void> {
const name = formatExportFileName('csv');

const selectedResults = results?.current?.map((row) =>
row !== undefined && f.has(selectedRows, row[0])
Expand All @@ -125,6 +135,148 @@ export function QueryExportButtons({
);
}

async function exportKmlSelected(): Promise<void> {
const name = formatExportFileName('kml');

const selectedResults = results?.current?.map((row) =>
row !== undefined && f.has(selectedRows, row[0])
? row?.slice(1).map((cell) => cell?.toString() ?? '')
: undefined
);

if (selectedResults === undefined) return undefined;

const filteredResults = filterArray(selectedResults);

const columnsName = fields
.filter((field) => field.isDisplay)
.map((field) =>
generateMappingPathPreview(baseTableName, field.mappingPath)
);


let placemarkTarget: any = [];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reduce usages of any.
in this case the type XmlNode might be better


filteredResults?.forEach((result: any) => {
let dataTarget: any = [];

// <ExtendedData>
let extendedDataTarget: any = [];

fields.forEach((field, index) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could be implemented using .map() too

see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map

that would match the code style in the rest of the codebase and reduce the need for explicitly specifying typescript types

const fieldValue = result?.[index + 1];

extendedDataTarget.push({
tagName: "Data",
attributes: { name: columnsName[index + 1] },
children: [
{
tagName: "value",
attributes: {},
children: [
{
tagName: "value",
attributes: {},
children: [],
type: "Text",
string: String(fieldValue)
}
],
}
]
});
});

let extendedData: XmlNode = {
tagName: "ExtendedData",
attributes: {},
children: extendedDataTarget as ReadonlyArray<XmlNode>
};

// push
dataTarget.push(extendedData);

// <name>
const nameValue = fields.map((field) => result?.[field.id]).join(' - ');
let nameData: XmlNode = {
tagName: "name",
attributes: {},
children: [
{
tagName: "name",
attributes: {},
children: [],
type: "Text",
string: nameValue
}
],
};
// push
dataTarget.push(nameData);

// <Point>
const coordinatesValue = fields
.filter(
(field) =>
field.mappingPath.toString().includes('latitude') ||
field.mappingPath.toString().includes('longitude')
)
.map((field) => result?.[field.id])
.join(', ');

let pointData: XmlNode = {
tagName: "Point",
attributes: {},
children: [
{
tagName: "coordinates",
attributes: {},
children: [
{
tagName: "coordinates",
attributes: {},
children: [],
type: "Text",
string: coordinatesValue
}
],
}
]
};
// push
dataTarget.push(pointData);

let placemark: XmlNode = {
tagName: "Placemark",
attributes: {},
children: dataTarget as ReadonlyArray<XmlNode>
};

// Insert placemark into document (target)
placemarkTarget.push(placemark);
});

let jsonData: XmlNode = {
tagName: "kml",
attributes: {
xmlns: "http://earth.google.com/kml/2.2"
},
children: [
{
tagName: "Document",
attributes: {},
children: placemarkTarget as ReadonlyArray<XmlNode>
}
]
};

const xmlElement = jsonToXml(jsonData);
const serializer = new XMLSerializer();
const xmlString = '<?xml version="1.0" encoding="utf-8"?>\n' + serializer.serializeToString(xmlElement);

return downloadFile(name, xmlString);
}

const containsResults = results.current?.some((row) => row !== undefined);

const canUseKml =
Expand Down Expand Up @@ -159,7 +311,7 @@ export function QueryExportButtons({
onClick={(): void => {
selectedRows.size === 0
? doQueryExport('/stored_query/exportcsv/', separator, utf8Bom)
: exportSelected().catch(softFail);
: exportCsvSelected().catch(softFail);
}}
>
{queryText.createCsv()}
Expand All @@ -171,7 +323,11 @@ export function QueryExportButtons({
showConfirmation={showConfirmation}
onClick={(): void =>
hasLocalityColumns(fields)
? doQueryExport('/stored_query/exportkml/', undefined, undefined)
? (
selectedRows.size === 0
? doQueryExport('/stored_query/exportkml/', undefined, undefined)
: exportKmlSelected().catch(softFail)
)
: setState('warning')
}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export const xmlToJson = (element: Element): XmlNode => ({
/**
* Reverse conversion to JSON
*/
export function jsonToXml(node: XmlNode): Element {
export function jsonToXml(node: any): Element {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please refrain from using any in our codebase.
any disabled type-checking, which negates the benefits of TypeScript

you can read a quick reference guide on TypeScript if you wish to be more familiar with TypeScript concepts: https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html
you can also use chatgpt to explain TypeScript errors

if you are using VS Code, you can also install this extension to make the error messages more readable: https://marketplace.visualstudio.com/items?itemName=yoavbls.pretty-ts-errors

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aha, I didn't think my code would be reviewed yet 😅 I promise this was a temporary hack as I was still figuring out the xml types.

Copy link
Collaborator

@realVinayak realVinayak Oct 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that's my mistake, I didn't mean to trigger a code review. Nevertheless, these resources by Max are good starting points!

const xmlDocument = document.implementation.createDocument(null, null);
const element = xmlDocument.createElement(node.tagName);
xmlDocument.append(element);
Expand Down
Loading