forked from MapComplete/MapComplete
		
	
		
			
				
	
	
		
			187 lines
		
	
	
	
		
			7.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			187 lines
		
	
	
	
		
			7.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import Combine from "../Base/Combine"
 | 
						|
import { Store, Stores } from "../../Logic/UIEventSource"
 | 
						|
import Translations from "../i18n/Translations"
 | 
						|
import { SubtleButton } from "../Base/SubtleButton"
 | 
						|
import { VariableUiElement } from "../Base/VariableUIElement"
 | 
						|
import Title from "../Base/Title"
 | 
						|
import InputElementMap from "../Input/InputElementMap"
 | 
						|
import BaseUIElement from "../BaseUIElement"
 | 
						|
import FileSelectorButton from "../Input/FileSelectorButton"
 | 
						|
import { FlowStep } from "./FlowStep"
 | 
						|
import { parse } from "papaparse"
 | 
						|
import { FixedUiElement } from "../Base/FixedUiElement"
 | 
						|
import { TagUtils } from "../../Logic/Tags/TagUtils"
 | 
						|
 | 
						|
class FileSelector extends InputElementMap<FileList, { name: string; contents: Promise<string> }> {
 | 
						|
    constructor(label: BaseUIElement) {
 | 
						|
        super(
 | 
						|
            new FileSelectorButton(label, { allowMultiple: false, acceptType: "*" }),
 | 
						|
            (x0, x1) => {
 | 
						|
                // Total hack: x1 is undefined is the backvalue - we effectively make this a one-way-story
 | 
						|
                return x1 === undefined || x0 === x1
 | 
						|
            },
 | 
						|
            (filelist) => {
 | 
						|
                if (filelist === undefined) {
 | 
						|
                    return undefined
 | 
						|
                }
 | 
						|
                const file = filelist.item(0)
 | 
						|
                return { name: file.name, contents: file.text() }
 | 
						|
            },
 | 
						|
            (_) => undefined
 | 
						|
        )
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * The first step in the import flow: load a file and validate that it is a correct geojson or CSV file
 | 
						|
 */
 | 
						|
export class RequestFile extends Combine implements FlowStep<{ features: any[] }> {
 | 
						|
    public readonly IsValid: Store<boolean>
 | 
						|
    /**
 | 
						|
     * The loaded GeoJSON
 | 
						|
     */
 | 
						|
    public readonly Value: Store<{ features: any[] }>
 | 
						|
 | 
						|
    constructor() {
 | 
						|
        const t = Translations.t.importHelper.selectFile
 | 
						|
        const csvSelector = new FileSelector(new SubtleButton(undefined, t.description))
 | 
						|
        const loadedFiles = new VariableUiElement(
 | 
						|
            csvSelector.GetValue().map((file) => {
 | 
						|
                if (file === undefined) {
 | 
						|
                    return t.noFilesLoaded.SetClass("alert")
 | 
						|
                }
 | 
						|
                return t.loadedFilesAre.Subs({ file: file.name }).SetClass("thanks")
 | 
						|
            })
 | 
						|
        )
 | 
						|
 | 
						|
        const text = Stores.flatten(
 | 
						|
            csvSelector.GetValue().map((v) => {
 | 
						|
                if (v === undefined) {
 | 
						|
                    return undefined
 | 
						|
                }
 | 
						|
                return Stores.FromPromise(v.contents)
 | 
						|
            })
 | 
						|
        )
 | 
						|
 | 
						|
        const asGeoJson: Store<any | { error: string | BaseUIElement }> = text.map(
 | 
						|
            (src: string) => {
 | 
						|
                if (src === undefined) {
 | 
						|
                    return undefined
 | 
						|
                }
 | 
						|
                try {
 | 
						|
                    const parsed = JSON.parse(src)
 | 
						|
                    if (parsed["type"] !== "FeatureCollection") {
 | 
						|
                        return { error: t.errNotFeatureCollection }
 | 
						|
                    }
 | 
						|
                    if (parsed.features.some((f) => f.geometry.type != "Point")) {
 | 
						|
                        return { error: t.errPointsOnly }
 | 
						|
                    }
 | 
						|
                    parsed.features.forEach((f) => {
 | 
						|
                        const props = f.properties
 | 
						|
                        for (const key in props) {
 | 
						|
                            if (
 | 
						|
                                props[key] === undefined ||
 | 
						|
                                props[key] === null ||
 | 
						|
                                props[key] === ""
 | 
						|
                            ) {
 | 
						|
                                delete props[key]
 | 
						|
                            }
 | 
						|
                            if (!TagUtils.isValidKey(key)) {
 | 
						|
                                return { error: "Probably an invalid key: " + key }
 | 
						|
                            }
 | 
						|
                        }
 | 
						|
                    })
 | 
						|
                    return parsed
 | 
						|
                } catch (e) {
 | 
						|
                    // Loading as CSV
 | 
						|
                    var lines: string[][] = <any>parse(src).data
 | 
						|
                    const header = lines[0]
 | 
						|
                    lines.splice(0, 1)
 | 
						|
                    if (header.indexOf("lat") < 0 || header.indexOf("lon") < 0) {
 | 
						|
                        return { error: t.errNoLatOrLon }
 | 
						|
                    }
 | 
						|
 | 
						|
                    if (header.some((h) => h.trim() == "")) {
 | 
						|
                        return { error: t.errNoName }
 | 
						|
                    }
 | 
						|
 | 
						|
                    if (new Set(header).size !== header.length) {
 | 
						|
                        return { error: t.errDuplicate }
 | 
						|
                    }
 | 
						|
 | 
						|
                    const features = []
 | 
						|
                    for (let i = 0; i < lines.length; i++) {
 | 
						|
                        const attrs = lines[i]
 | 
						|
                        if (attrs.length == 0 || (attrs.length == 1 && attrs[0] == "")) {
 | 
						|
                            // empty line
 | 
						|
                            continue
 | 
						|
                        }
 | 
						|
                        const properties = {}
 | 
						|
                        for (let i = 0; i < header.length; i++) {
 | 
						|
                            const v = attrs[i]
 | 
						|
                            if (v === undefined || v === "") {
 | 
						|
                                continue
 | 
						|
                            }
 | 
						|
                            properties[header[i]] = v
 | 
						|
                        }
 | 
						|
                        const coordinates = [Number(properties["lon"]), Number(properties["lat"])]
 | 
						|
                        delete properties["lat"]
 | 
						|
                        delete properties["lon"]
 | 
						|
                        if (coordinates.some(isNaN)) {
 | 
						|
                            return { error: "A coordinate could not be parsed for line " + (i + 2) }
 | 
						|
                        }
 | 
						|
                        const f = {
 | 
						|
                            type: "Feature",
 | 
						|
                            properties,
 | 
						|
                            geometry: {
 | 
						|
                                type: "Point",
 | 
						|
                                coordinates,
 | 
						|
                            },
 | 
						|
                        }
 | 
						|
                        features.push(f)
 | 
						|
                    }
 | 
						|
 | 
						|
                    return {
 | 
						|
                        type: "FeatureCollection",
 | 
						|
                        features,
 | 
						|
                    }
 | 
						|
                }
 | 
						|
            }
 | 
						|
        )
 | 
						|
 | 
						|
        const errorIndicator = new VariableUiElement(
 | 
						|
            asGeoJson.map((v) => {
 | 
						|
                if (v === undefined) {
 | 
						|
                    return undefined
 | 
						|
                }
 | 
						|
                if (v?.error === undefined) {
 | 
						|
                    return undefined
 | 
						|
                }
 | 
						|
                let err: BaseUIElement
 | 
						|
                if (typeof v.error === "string") {
 | 
						|
                    err = new FixedUiElement(v.error)
 | 
						|
                } else if (v.error.Clone !== undefined) {
 | 
						|
                    err = v.error.Clone()
 | 
						|
                } else {
 | 
						|
                    err = v.error
 | 
						|
                }
 | 
						|
                return err.SetClass("alert")
 | 
						|
            })
 | 
						|
        )
 | 
						|
 | 
						|
        super([
 | 
						|
            new Title(t.title, 1),
 | 
						|
            t.fileFormatDescription,
 | 
						|
            t.fileFormatDescriptionCsv,
 | 
						|
            t.fileFormatDescriptionGeoJson,
 | 
						|
            csvSelector,
 | 
						|
            loadedFiles,
 | 
						|
            errorIndicator,
 | 
						|
        ])
 | 
						|
        this.SetClass("flex flex-col wi")
 | 
						|
        this.IsValid = asGeoJson.map(
 | 
						|
            (geojson) => geojson !== undefined && geojson["error"] === undefined
 | 
						|
        )
 | 
						|
        this.Value = asGeoJson
 | 
						|
    }
 | 
						|
}
 |