XmlParser.java

package org.opentrafficsim.road.network.factory.xml.parser;

import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.transform.sax.SAXSource;

import org.djunits.value.vdouble.scalar.Direction;
import org.djunits.value.vdouble.scalar.Duration;
import org.djunits.value.vdouble.scalar.Speed;
import org.djutils.eval.Eval;
import org.djutils.exceptions.Throw;
import org.djutils.io.URLResource;
import org.djutils.logger.CategoryLogger;
import org.opentrafficsim.base.logger.Cat;
import org.opentrafficsim.base.parameters.ParameterType;
import org.opentrafficsim.core.definitions.Definitions;
import org.opentrafficsim.core.distributions.Distribution.FrequencyAndObject;
import org.opentrafficsim.core.dsol.OtsSimulator;
import org.opentrafficsim.core.dsol.OtsSimulatorInterface;
import org.opentrafficsim.core.geometry.ContinuousLine;
import org.opentrafficsim.core.geometry.Flattener;
import org.opentrafficsim.core.geometry.OtsGeometryException;
import org.opentrafficsim.core.gtu.GtuException;
import org.opentrafficsim.core.gtu.GtuType;
import org.opentrafficsim.core.network.LinkType;
import org.opentrafficsim.core.network.NetworkException;
import org.opentrafficsim.core.network.route.Route;
import org.opentrafficsim.core.parameters.ParameterFactory;
import org.opentrafficsim.road.gtu.generator.GeneratorPositions.LaneBias;
import org.opentrafficsim.road.gtu.generator.LaneBasedGtuGenerator;
import org.opentrafficsim.road.gtu.strategical.LaneBasedStrategicalPlannerFactory;
import org.opentrafficsim.road.network.RoadNetwork;
import org.opentrafficsim.road.network.factory.xml.XmlParserException;
import org.opentrafficsim.trafficcontrol.TrafficControlException;
import org.opentrafficsim.xml.generated.Demand;
import org.opentrafficsim.xml.generated.GtuTemplate;
import org.opentrafficsim.xml.generated.ModelType;
import org.opentrafficsim.xml.generated.Network;
import org.opentrafficsim.xml.generated.Ots;
import org.opentrafficsim.xml.generated.RoadLayout;
import org.opentrafficsim.xml.generated.ScenarioType;
import org.pmw.tinylog.Level;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;

import nl.tudelft.simulation.dsol.SimRuntimeException;
import nl.tudelft.simulation.dsol.experiment.ExperimentRunControl;
import nl.tudelft.simulation.dsol.experiment.StreamSeedInformation;

/**
 * Parse an XML file for OTS, based on the ots.xsd definition.
 * <p>
 * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
 * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
 * </p>
 * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
 * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
 */
public final class XmlParser implements Serializable
{
    /** */
    private static final long serialVersionUID = 2019022L;

    /** Road network. */
    private final RoadNetwork network;

    /** Stream with the XML information. */
    private InputStream stream;

    /** Scenario to parse. */
    private String scenario;

    /** Whether to parse conflicts. */
    private boolean parseConflicts;

    /**
     * Constructor.
     * @param network RoadNetwork; network.
     */
    public XmlParser(final RoadNetwork network)
    {
        this.network = network;
    }

    /**
     * Set file name.
     * @param filename String; file name.
     * @return this parser for method chaining.
     * @throws IllegalStateException file, URL or stream has already been set.
     * @throws IOException file could not be opened.
     */
    public XmlParser setFile(final String filename) throws IOException
    {
        Throw.when(this.stream != null, IllegalStateException.class, "Invoke only one of setFile(), setUrl(), or setStream().");
        this.stream = URLResource.getResource(filename).openStream();
        return this;
    }

    /**
     * Set url.
     * @param url URL; url.
     * @return this parser for method chaining.
     * @throws IllegalStateException file, URL or stream has already been set.
     * @throws IOException file could not be opened.
     */
    public XmlParser setUrl(final URL url) throws IOException
    {
        Throw.when(this.stream != null, IllegalStateException.class, "Invoke only one of setFile(), setUrl(), or setStream().");
        this.stream = url.openStream();
        return this;
    }

    /**
     * Set stream.
     * @param stream InputStream; stream.
     * @return this parser for method chaining.
     * @throws IllegalStateException file, URL or stream has already been set.
     */
    public XmlParser setStream(final InputStream stream)
    {
        Throw.when(this.stream != null, IllegalStateException.class, "Invoke only one of setFile(), setUrl(), or setStream().");
        this.stream = stream;
        return this;
    }

    /**
     * Set scenario to parse.
     * @param scenario String; name of scenario to parse.
     * @return this parser for method chaining.
     */
    public XmlParser setScenario(final String scenario)
    {
        this.scenario = scenario;
        return this;
    }

    /**
     * Set whether to parse conflicts.
     * @param parseConflicts boolean; whether to parse conflicts.
     * @return this parser for method chaining.
     */
    public XmlParser setParseConflict(final boolean parseConflicts)
    {
        this.parseConflicts = parseConflicts;
        return this;
    }

    /**
     * Build the simulation.
     * @return the experiment based on the information in the Run tag
     * @throws IllegalStateException when no file, url or stream was set.
     * @throws JAXBException when the parsing fails
     * @throws URISyntaxException when the filename is not valid
     * @throws NetworkException when the objects cannot be inserted into the network due to inconsistencies
     * @throws OtsGeometryException when the design line of a link is invalid
     * @throws XmlParserException when the stripe type cannot be recognized
     * @throws ParserConfigurationException on error with parser configuration
     * @throws SAXException on error creating SAX parser
     * @throws SimRuntimeException in case of simulation problems building the car generator
     * @throws GtuException when construction of the Strategical Planner failed
     * @throws TrafficControlException when construction of a traffic controller fails
     * @throws IOException when construction of a traffic controller fails
     * @throws MalformedURLException when construction of a traffic controller fails
     */
    public ExperimentRunControl<Duration> build() throws SimRuntimeException, MalformedURLException, JAXBException,
            URISyntaxException, NetworkException, OtsGeometryException, XmlParserException, SAXException,
            ParserConfigurationException, GtuException, IOException, TrafficControlException
    {
        Throw.when(this.stream == null, IllegalStateException.class,
                "Invoke one of setFile(), setUrl(), or setStream() before parsing.");
        return build(parseXml(this.stream), this.network, this.scenario, this.parseConflicts);
    }

    /**
     * Parse an OTS XML input stream and build an OTS object.
     * @param xmlStream inputStream; the xml stream
     * @return Ots; the constructed OTS object
     * @throws JAXBException when the parsing fails
     * @throws ParserConfigurationException on error with parser configuration
     * @throws SAXException on error creating SAX parser
     */
    private static Ots parseXml(final InputStream xmlStream) throws JAXBException, SAXException, ParserConfigurationException
    {
        Locale locale = Locale.getDefault();
        Locale.setDefault(Locale.US);
        JAXBContext jc = JAXBContext.newInstance(Ots.class);
        Unmarshaller unmarshaller = jc.createUnmarshaller();
        SAXParserFactory spf = SAXParserFactory.newInstance();
        spf.setXIncludeAware(true);
        spf.setNamespaceAware(true);
        spf.setValidating(false);
        XMLReader xmlReader = spf.newSAXParser().getXMLReader();
        xmlReader.setEntityResolver(new DefaultsResolver());
        SAXSource saxSource = new SAXSource(xmlReader, new InputSource(xmlStream));
        Ots result = (Ots) unmarshaller.unmarshal(saxSource);
        Locale.setDefault(locale);
        return result;
    }

    /**
     * Build the network from an OTS object (probably constructed by parsing an OTS XML file; e.g. the parseXML method).
     * @param ots Ots; the OTS object
     * @param otsNetwork RoadNetwork; the network to insert the parsed objects in
     * @param scenario String; scenario name, may bee {@code null} to use default values.
     * @param buildConflicts boolean; whether to build conflicts or not
     * @return the experiment based on the information in the RUN tag
     * @throws JAXBException when the parsing fails
     * @throws URISyntaxException when the filename is not valid
     * @throws NetworkException when the objects cannot be inserted into the network due to inconsistencies
     * @throws OtsGeometryException when the design line of a link is invalid
     * @throws XmlParserException when the stripe type cannot be recognized
     * @throws ParserConfigurationException on error with parser configuration
     * @throws SAXException on error creating SAX parser
     * @throws SimRuntimeException in case of simulation problems building the car generator
     * @throws GtuException when construction of the Strategical Planner failed
     * @throws TrafficControlException when construction of a traffic controller fails
     * @throws IOException when construction of a traffic controller fails
     * @throws MalformedURLException when construction of a traffic controller fails
     */
    private static ExperimentRunControl<Duration> build(final Ots ots, final RoadNetwork otsNetwork, final String scenario,
            final boolean buildConflicts) throws JAXBException, URISyntaxException, NetworkException, OtsGeometryException,
            XmlParserException, SAXException, ParserConfigurationException, SimRuntimeException, GtuException,
            MalformedURLException, IOException, TrafficControlException
    {
        CategoryLogger.setLogCategories(Cat.PARSER);
        CategoryLogger.setAllLogLevel(Level.TRACE);

        // input parameters
        Eval eval = ScenarioParser.parseInputParameters(ots.getScenarios(), scenario);

        // run
        StreamSeedInformation streamInformation = RunParser.parseStreams(ots.getRun(), eval);
        ExperimentRunControl<Duration> runControl =
                RunParser.parseRun(otsNetwork.getId(), ots.getRun(), streamInformation, otsNetwork.getSimulator(), eval);

        // definitions
        Map<String, RoadLayout> roadLayoutMap = new LinkedHashMap<>();
        Map<String, GtuTemplate> gtuTemplates = new LinkedHashMap<>();
        Map<String, LaneBias> laneBiases = new LinkedHashMap<>();
        Map<LinkType, Map<GtuType, Speed>> linkTypeSpeedLimitMap = new LinkedHashMap<>();
        Definitions definitions = DefinitionsParser.parseDefinitions(ots.getDefinitions(), roadLayoutMap, gtuTemplates,
                laneBiases, linkTypeSpeedLimitMap, eval);

        // network
        Network network = ots.getNetwork();
        Map<String, Direction> nodeDirections = NetworkParser.calculateNodeAngles(otsNetwork, network, eval);
        NetworkParser.parseNodes(otsNetwork, network, nodeDirections, eval);
        Map<String, ContinuousLine> designLines = new LinkedHashMap<>();
        Map<String, Flattener> flatteners = new LinkedHashMap<>();
        NetworkParser.parseLinks(otsNetwork, definitions, network, nodeDirections, otsNetwork.getSimulator(), designLines,
                flatteners, eval);
        NetworkParser.applyRoadLayout(otsNetwork, definitions, network, otsNetwork.getSimulator(), roadLayoutMap,
                linkTypeSpeedLimitMap, designLines, flatteners, eval);
        NetworkParser.buildConflicts(otsNetwork, network, eval);

        // routes, generators and sinks
        Demand demand = ots.getDemand();
        if (demand != null)
        {
            DemandParser.parseRoutes(otsNetwork, definitions, demand, eval);
            DemandParser.parseShortestRoutes(otsNetwork, definitions, demand, eval);
            Map<String, List<FrequencyAndObject<Route>>> routeMixMap = DemandParser.parseRouteMix(otsNetwork, demand, eval);
            Map<String, List<FrequencyAndObject<Route>>> shortestRouteMixMap =
                    DemandParser.parseShortestRouteMix(otsNetwork, demand, eval);
            List<LaneBasedGtuGenerator> generators = DemandParser.parseGenerators(otsNetwork, definitions, demand, gtuTemplates,
                    routeMixMap, shortestRouteMixMap, streamInformation, eval);
            System.out.println("Created " + generators.size() + " generators based on explicit generator definitions");
            DemandParser.parseSinks(otsNetwork, demand, otsNetwork.getSimulator(), definitions, eval);
        }

        // models and parameters
        // TODO: we now only take the first model, need to make models per GTU type, and with parents
        List<ModelType> models = ots.getModels() == null ? new ArrayList<>() : ots.getModels().getModel();

        Map<String, ParameterType<?>> parameterTypes = new LinkedHashMap<>();
        DefinitionsParser.parseParameterTypes(ots.getDefinitions(), parameterTypes, eval);
        ParameterFactory parameterFactory =
                ModelParser.parseParameters(definitions, models, eval, parameterTypes, streamInformation);
        Map<String, LaneBasedStrategicalPlannerFactory<?>> factories =
                ModelParser.parseModel(otsNetwork, models, eval, parameterTypes, streamInformation, parameterFactory);
        List<ScenarioType> scenarios = ots.getScenarios() == null ? new ArrayList<>() : ots.getScenarios().getScenario();
        if (demand != null)
        {
            Map<String, String> modelIdReferrals = ScenarioParser.parseModelIdReferral(scenarios, demand, eval);

            // OD generators
            List<LaneBasedGtuGenerator> generators = OdParser.parseDemand(otsNetwork, definitions, demand, gtuTemplates,
                    laneBiases, factories, modelIdReferrals, streamInformation, eval);
            System.out.println("Created " + generators.size() + " generators based on origin destination matrices");
        }

        // control
        if (ots.getControl() != null)
        {
            ControlParser.parseControl(otsNetwork, otsNetwork.getSimulator(), ots.getControl(), definitions, eval);
        }

        return runControl;
    }

    /**
     * DefaultsResolver takes care of locating the defaults include files at the right place.
     * <p>
     * Copyright (c) 2023-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
     * <br>
     * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
     * </p>
     * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
     * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
     * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
     */
    private static class DefaultsResolver implements EntityResolver
    {
        /** {@inheritDoc} */
        @Override
        public InputSource resolveEntity(final String publicId, final String systemId)
        {
            if (systemId.contains("defaults/"))
            {
                String location = "/resources/xsd/defaults" + systemId.substring(systemId.lastIndexOf('/'));
                InputStream stream = URLResource.getResourceAsStream(location);
                return new InputSource(stream);
            }
            else
            {
                return new InputSource(URLResource.getResourceAsStream(systemId));
            }
        }
    }

    /**
     * @param args String[]; not used
     * @throws Exception on parsing error
     */
    public static void main(final String[] args) throws Exception
    {
        OtsSimulatorInterface simulator = new OtsSimulator("XmlNetworkLaneParser");
        new XmlParser(new RoadNetwork("", simulator)).setFile("/example.xml").build();
        System.exit(0);
    }

}